IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> socket编程:多线程建立一对多的tcp服务 -> 正文阅读

[网络协议]socket编程:多线程建立一对多的tcp服务

前言

因为编程需要,需要将tcp服务增加到软件中进行本地测试,所以开始学习socket编程,首先需要了解socket相关的知识。本文只摘录编程中最先需要了解的内容,详细请参考以下两个博客:
https://blog.csdn.net/xiaoquantouer/article/details/58001960
https://www.cnblogs.com/helloworldcode/p/10610581.html
sockets(套接字)编程有三种,流式套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW);基于TCP的socket编程是采用的流式套接字。

编程模式

客户端/服务端模式:

在TCP/IP网络应用中,通信的两个进程相互作用的主要模式是客户/服务器模式,即客户端向服务器发出请求,服务器接收请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:

(1)建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而就让拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。

(2)网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区。

因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于客户/服务端模式的TCP/IP。

服务端:建立socket,声明自身的端口号和地址并绑定到socket,使用listen打开监听,然后不断用accept去查看是否有连接,如果有,捕获socket,并通过recv获取消息的内容,通信完成后调用closeSocket关闭这个对应accept到的socket,如果不再需要等待任何客户端连接,那么用closeSocket关闭掉自身的socket。

客户端:建立socket,通过端口号和地址确定目标服务器,使用Connect连接到服务器,send发送消息,等待处理,通信完成后调用closeSocket关闭socket。

编程步骤

(1)服务端

1、加载套接字库,创建套接字(WSAStartup()/socket());

2、绑定套接字到一个IP地址和一个端口上(bind());

3、将套接字设置为监听模式等待连接请求(listen());

4、请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());

5、用返回的套接字和客户端进行通信(send()/recv());

6、返回,等待另一个连接请求;

7、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());

(2)客户端

1、加载套接字库,创建套接字(WSAStartup()/socket());

2、向服务器发出连接请求(connect());

3、和服务器进行通信(send()/recv());

4、关闭套接字,关闭加载的套接字库(closesocket()/WSACleanup());

一对一的TCP服务

讲解socket编程的博客和代码并不少,我也参考了不少的博客,最终选择了https://www.cnblogs.com/zhangaihua/p/3718089.html,因为其中的注释较多,在服务器端和客户端代码的重要步骤做了解释,方便理解;此外其差错控制也相当细,当TCP服务出现问题时,可以及时获取到错误问题,以下是其修改格式的代码:

Server.cpp

#include<WINSOCK2.H>
#include<iostream>
using namespace std;
#include<stdlib.h>
#define BUF_SIZE 64
#pragma comment(lib,"WS2_32.lib")
int main()
{
	WSADATA wsd;
	SOCKET sServer;
	SOCKET sClient;
	int retVal;//调用Socket函数的返回值
	char buf[BUF_SIZE];
	//初始化Socket环境
	if(WSAStartup(MAKEWORD(2,2),&wsd)!=0){
		printf("WSAStartup failed!\n");
		return 1;
	}
	//创建监听的Socket
	sServer=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if(INVALID_SOCKET==sServer){
		printf("socket failed!\n");
		WSACleanup();
		return -1;
	}
	//设置服务器Socket地址
	SOCKADDR_IN addrServ;
	addrServ.sin_family=AF_INET;
	addrServ.sin_port=htons(1298);
	addrServ.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
	//绑定Sockets Server
	retVal=bind(sServer,(const struct sockaddr*)&addrServ,sizeof(SOCKADDR_IN));
	if(SOCKET_ERROR==retVal){
		printf("bind failed!\n");
		closesocket(sServer);
		WSACleanup();
		return -1;
	}
	//在Sockets Server上进行监听
	retVal=listen(sServer,1);
	if(SOCKET_ERROR==retVal){
		printf("listen failed!\n");
		closesocket(sServer);
		WSACleanup();
		return -1;
	}
	//接收来自客户端的请求
	printf("TCPServer start...\n");
	sockaddr_in addrClient;
	int addrClientlen=sizeof(addrClient);
	sClient=accept(sServer,(sockaddr FAR*)&addrClient,&addrClientlen);
	if(INVALID_SOCKET==sClient){
		printf("accept failed!\n");
		closesocket(sServer);
		WSACleanup();
		return -1;
	}
	//循环接收客户端的数据,直接客户端发送quit命令后退出
	while(true){
		ZeroMemory(buf,BUF_SIZE);//清空接收数据的缓冲区
		retVal=recv(sClient,buf,BUFSIZ,0);
		if(SOCKET_ERROR==retVal){
			printf("recv failed!\n");
			closesocket(sServer);
			closesocket(sClient);
			WSACleanup();
			return -1;
		}
		//获取当前系统时间
		SYSTEMTIME st;
		GetLocalTime(&st);
		char sDateTime[30];
		sprintf(sDateTime,"%4d-%2d-%2d-%2d:%2d:%2d:%2d",st.wYear,st.wMonth,st.wDay,st.wHour,st.wMinute,st.wSecond);
		//打印输出的信息
		printf("%s,Recv From Client [%s:%d]:%s\n",sDateTime,inet_ntoa(addrClient.sin_addr),addrClient.sin_port,buf);
		//如果客户端发送quit字符串,则服务器退出
		if(strcmp(buf,"quit")==0){
			retVal=send(sClient,"quit",strlen("quit"),0);
			break;
		}
		//否则向客户端发送回显字符串
		else{
			char msg[BUF_SIZE];
			sprintf(msg,"Message received - %s\n",buf);
			retVal=send(sClient,msg,strlen(msg),0);
			if(SOCKET_ERROR==retVal){
				printf("send failed!\n");
				closesocket(sServer);
				closesocket(sClient);
				WSACleanup();
				return -1;
			}
			
		}
	}
	//释放Socket
	closesocket(sServer);
	closesocket(sClient);
	WSACleanup();
	//暂停,按任意键退出
	system("pause");
	return 0;
}

Client.cpp

#include<WINSOCK2.H>
#include<iostream>
#include<string>
using namespace std;
#include<stdlib.h>
#define BUF_SIZE 64
#pragma comment(lib,"WS2_32.lib")
int main()
{
	WSADATA wsd;
	SOCKET sHost;
	SOCKADDR_IN servAddr;//服务器地址
	int retVal;//调用Socket函数的返回值
	char buf[BUF_SIZE];
	//初始化Socket环境
	if(WSAStartup(MAKEWORD(2,2),&wsd)!=0){
		printf("WSAStartup failed!\n");
		return 1;
	}
	//创建监听的Socket
	sHost=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if(INVALID_SOCKET==sHost){
		printf("socket failed!\n");
		WSACleanup();
		return -1;
	}
	//设置服务器Socket地址
	servAddr.sin_family=AF_INET;
	servAddr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
	//在实际应用中,建议将服务器的IP地址和端口号保存在配置文件中
	servAddr.sin_port=htons(1298);
	//计算地址的长度
	int sServerAddlen=sizeof(servAddr);
	//链接服务器
	retVal=connect(sHost,(LPSOCKADDR)&servAddr,sizeof(servAddr));
	if(SOCKET_ERROR==retVal){
		printf("send failed!\n");
		closesocket(sHost);
		WSACleanup();
		return -1;
	}
	while(true){
		//向服务器发送字符串,并显示反馈信息
		printf("input a string to send:");
		std::string str;
		//接收输入的数据
		std::getline(std::cin,str);
		//将用户输入的数据复制到buf中
		ZeroMemory(buf,BUF_SIZE);
		strcpy(buf,str.c_str());
		//向服务器发送数据
		retVal=send(sHost,buf,strlen(buf),0);
		if(SOCKET_ERROR==retVal){
			printf("send failed!\n");
			closesocket(sHost);
			WSACleanup();
			return -1;
		}
		//接收服务器回传的数据
		retVal=recv(sHost,buf,sizeof(buf)+1,0);
		printf("Recv From Server : %s\n",buf);
		if(strcmp(buf,"quit")==0){
			printf("quit!\n");
			break;
		}
	}
	return 0;
}

我是在DEV C++环境跑的程序,可能会遇到问题:undefined reference to ‘_imp_WSAStartup’

解决方法:

工具->编译选项->在连接器命令行加入如下命令里面添加-lwsock32 即可
然后devc++重新构建运行程序问题解决。

一对多的TCP服务

要想实现一对多的TCP服务,开通不同的端口用于一个client连接是不现实的,而且服务端的端口,能够接收多个用户连接的,但是每次都需要连接服务端的套接字,而且accep函数是阻塞函数(只有当有用户连接时才能唤醒),同样是阻塞函数的还有接收数据的函数recv(只有当有数据接收到才能唤醒)。那我们就可以在服务端的第四步进行循环监听连接请求,并每次连接上后开辟一个线程进行tcp收发数据服务。
原先的Client.cpp并不需要额外的改动,Server.cpp要进行较多的改动。改动的地方主要有以下:

  1. listen的限制数量增多
  2. 其中一个Client连接断开时,不能将Server释放,因为可能有其他的Client还在连接或者有其他Client将要连接
  3. 开辟的线程中可能需要主线程的参数,传入的参数可能需要用结构体包装

除此之外,一般来说Tcp服务都是部署到后台默默地进行服务,只把连接到Client的交互开辟线程还远远不够,因为accept也是个阻塞函数,也可以放到一个线程里。以下是实现代码

M_Server.cpp

#include<WINSOCK2.H>
#include<iostream>
using namespace std;
#include<stdlib.h>
#define BUF_SIZE 64 //最大字节数 
#define QUEUE_SIZE 5 //最大连接数 
#pragma comment(lib,"WS2_32.lib")

struct Param {//参数结构体 
	SOCKET sServer;
	SOCKET sClient;
	sockaddr_in addrClient;
};
DWORD WINAPI mainThread(LPVOID lpargs);
DWORD WINAPI ThreadProcServerConmunicate(LPVOID lpParameter);


int main()
{
	
	int port=1298;
	CreateThread(NULL,0,mainThread,&port,0,NULL);
	
	
//	cin>>port;//阻塞函数控制进程存活时间
	Sleep(100000);//控制主进程存活时间100s,时间到后程序结束,TCP服务子进程也会全部释放掉 
	return 0;
}

DWORD WINAPI mainThread(LPVOID lpargs)
{
	int *p = (int *)lpargs;//端口号
	int port = *p;
	
	WSADATA wsd;	
	SOCKET sServer;
	SOCKET sClient;
	int retVal;//调用Socket函数的返回值
	char buf[BUF_SIZE];
	//初始化Socket环境
	if(WSAStartup(MAKEWORD(2,2),&wsd)!=0)
	{
		printf("init failed!\n");
		return 1;
	}
	//创建监听的Socket
	sServer=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if(INVALID_SOCKET==sServer)
	{
		printf("socket failed!\n");
		WSACleanup();
		return -1;
	}
	//设置服务器Socket地址
	SOCKADDR_IN addrServ;
	addrServ.sin_family=AF_INET;
	addrServ.sin_port=htons(port);
	addrServ.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
	//绑定Sockets Server
	retVal=bind(sServer,(const struct sockaddr*)&addrServ,sizeof(SOCKADDR_IN));

	if(SOCKET_ERROR==retVal)
	{
		printf("bind failed!\n");
		closesocket(sServer);
		WSACleanup();
		return -1;
	}
	//在Sockets Server上进行监听
	retVal=listen(sServer,QUEUE_SIZE);
	if(SOCKET_ERROR==retVal)
	{
		printf("listen failed!\n");
		closesocket(sServer);
		WSACleanup();
		return -1;
	}
	//接收来自客户端的请求
	printf("TCPServer start...\n");
	
	
	
	//循环接收客户端的数据,直接客户端发送quit命令后退出
	while(true)
	{			
		sockaddr_in addrClient;
		int addrClientlen=sizeof(addrClient);
		sClient=accept(sServer,(sockaddr FAR*)&addrClient,&addrClientlen);
	
		if(INVALID_SOCKET==sClient)
		{
			printf("accept failed!\n");
		//	closesocket(sServer);
			WSACleanup();
			system("pause");
			return -1;
		}
		else{
			printf("一个客户端已经连接到本机的%d端口,SOCKET是 : %u \n",port, sServer);
			
			Param param;//传入参数
			param.sServer=sServer;
			param.sClient=sClient;
			param.addrClient=addrClient; 
			
			
			CreateThread(
				NULL,
				0,
				ThreadProcServerConmunicate,
				&param,
				0,
				NULL
			);
		}
		
	}
	//释放Socket
	closesocket(sServer);
	closesocket(sClient);
	WSACleanup();
	//暂停,按任意键退出
	system("pause");
	return 0;
	
}
DWORD WINAPI ThreadProcServerConmunicate(LPVOID lpParameter) 
{
	int iPid = GetCurrentThreadId();//获得开辟的线程号 
	cout<<"The process id is: "<<iPid<<endl;
	
 	Param *param = (Param *)lpParameter;
 	
 	SOCKET sServer =param->sServer;
	SOCKET sClient =param->sClient;
	sockaddr_in addrClient =param->addrClient;
 	
 
	int retVal = 0;
	
	while (1)
	{
		char buf[BUF_SIZE];//清空接收数据的缓冲区
		retVal=recv(sClient,buf,BUF_SIZE,0);
		if(SOCKET_ERROR==retVal)
		{
			printf("recv failed!\n");
			//closesocket(sServer);
			closesocket(sClient);
			//WSACleanup();
			return -1;
		}
		//获取当前系统时间
		SYSTEMTIME st;
		GetLocalTime(&st);
		char sDateTime[30];
		sprintf(sDateTime,"%4d-%02d-%02d  %2d:%2d:%2d",st.wYear,st.wMonth,st.wDay,st.wHour,st.wMinute,st.wSecond);
		//打印输出的信息
		printf("%s,Recv From Client [%s:%d]:%s\n",sDateTime,inet_ntoa(addrClient.sin_addr),addrClient.sin_port,buf);
		
		//如果客户端发送quit字符串,则服务器退出
		if(strcmp(buf,"quit")==0)
		{
			retVal=send(sClient,"quit",strlen("quit"),0);
			break;
		}
		//否则向客户端发送回显字符串
		else
		{
			char msg[BUF_SIZE];
			sprintf(msg,"Message received - %s\n",buf);
			retVal=send(sClient,msg,strlen(msg),0);
			if(SOCKET_ERROR==retVal)
			{
				printf("send failed!\n");
				//closesocket(sServer);
				closesocket(sClient);
			//	WSACleanup();
				return -1;
			}
	
		}
 
	}

	//释放线程资源 
	pthread_detach(pthread_self());
	return 0;
}

最终效果:

在这里插入图片描述

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-08-19 12:24:57  更:2021-08-19 12:26:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 20:35:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码