基于TCP的多线程异步socket通信

基于TCP的多线程异步socket通信

1、服务端使用socket流程:
1)加载套接字库:WSAStartup (后面要给出具体函数的说明)

//加载套接字库
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	wVersionRequested = MAKEWORD(2, 2);   //表示调用版本2.2  wVersionRequested结果为512+2=514
	//加载套接字库
	err = WSAStartup(wVersionRequested, &wsaData);  //初始化程序所用的套接字
	if (err != 0) {  //wsaData用来接收套接字库的实现细节

		return FALSE;

}
2)版本号检测:

//版本号检测
	if (LOBYTE(wsaData.wVersion) != 2 ||   //副版本
		HIBYTE(wsaData.wVersion) != 2) {  //主版本

		WSACleanup();                       //终止程序对套接字库的使用
		return FALSE;
	}

3)创建套接字:socket

//创建套接字
m_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

4)绑定套接字:

//绑定套接字
int bindResult;
if (INVALID_SOCKET != m_socket)
{
	bindResult = bind(m_socket, (SOCKADDR*)&m_sockAddr, sizeof(SOCKADDR));
}
else
{
	MessageBox(_T("创建套接字失败!"));
	return FALSE;
}

5)监听套接字:
listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。该函数会阻塞。

//创建监听套接字进程	
if (0 == bindResult)
{
	m_listenResult = listen(m_socket, 20);                                     //返回值为0则监听成功
}
else
{
	MessageBox(_T("绑定套接字失败!"));
	return FALSE;
}

6)等待客户连接:accept

AfxBeginThread(AcceptThread, this, 0, 0, 0, THREAD_PRIORITY_NORMAL);

7)接收客户端数据:recv 该函数会阻塞。

 char recvBuf[MaxBufSize] = {0};
   int recvResult = recv(dlg->m_clientSocket, recvBuf, sizeof(recvBuf), 0);
注意:这里比较容易犯的错误是将接收的套接字m_clientSocket写成服务套接字m_socket。

8)向客户端发送数据:send

 char sendBuf[MaxBufSize] = {0};
    string sendTest; //这里用string而非CString是因为string型便于转换为const char* 类型
   int sendResult = send(dlg->m_clientSocket, sendBuf, sizeof(sendBuf), 0);
注意:用于发送的套接字是accept返回的套接字,而不是用于绑定端口和监听的套接字。

2、客户端使用socket流程
1)加载套接字库:

 //加载套接字库
	WORD wVersionRequested;
	WSADATA wsaData;
	int err;
	wVersionRequested = MAKEWORD(2, 2);   //表示调用版本2.2  wVersionRequested结果为512+2=514
										  
//加载套接字库
	err = WSAStartup(wVersionRequested, &wsaData);//初始化程序所用的套接字
	if (err != 0) { //wsaData用来接收套接字库的实现细节

		return FALSE;
	}

2)版本号检测:

//版本号检测
if (LOBYTE(wsaData.wVersion) != 2 ||   //副版本
	HIBYTE(wsaData.wVersion) != 2) {  //主版本

	WSACleanup();                       //终止程序对套接字库的使用
	return FALSE;
}

3)创建套接字:

 //创建重叠套接字
	m_socket = WSASocket(PF_INET, SOCK_STREAM, IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED); 
	//套接字初始化
	memset(&m_sockAddr, 0, sizeof(m_sockAddr));//每个字节都用0填充
	m_sockAddr.sin_family = AF_INET;//使用IPv4地址
	m_sockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");//具体的IP地址
	m_sockAddr.sin_port = htons(6000); 

4)连接套接字:

//向服务器发出连接请求
if (SOCKET_ERROR== connect(m_socket, (SOCKADDR*)&m_sockAddr, sizeof(SOCKADDR)))
{
MessageBox(_T(“连接失败!”));
return FALSE;
}

5)发送套接字库:

char recvBuf[1024] = { 0 };
recvResult = recv(dlg->m_socket, recvBuf, sizeof(recvBuf), 0);   

6)接收套接字库:
char recvBuf[1024] = { 0 };
recvResult = recv(dlg->m_socket, recvBuf, sizeof(recvBuf), 0);

3、服务器端和客户端通信图解

在这里插入图片描述

4、多线程异步socket通信
CSDN一博主对异步(Async)、同步(Sync)、阻塞(Block)、非阻塞(Unblock)的理解:异步方式指的是发送方不等接收方响应,便接着发下个数据包的通信方式;而同步指发送方发出数据后,等收到接收方发回的响应,才发下一个数据包的通信方式。阻塞套接字是指执行此套接字的网络调用时,直到成功才返回,否则一直阻塞在此网络调用上,比如调用recv()函数读取网络缓冲区中的数据,如果没有数据到达,将一直挂在recv()这个函数调用上,直到读到一些数据,此函数调用才返回;而非阻塞套接字是指执行此套接字的网络调用时,不管是否执行成功,都立即返回。比如调用recv()函数读取网络缓冲区中数据,不管是否读到数据都立即返回,而不会一直挂在此函数调用上。
个人理解:阻塞和非阻塞是现象,异步和同步是产生这种现象的手段。
1)将套接字设置为非阻塞有两种方式:
方式一:在创建套接字后调用函数ioctlsocket强制将其定义为非阻塞模式,如下:

//定义成非阻塞
int per = 1;
if (ioctlsocket(m_socket, FIONBIO, (u_long *)&per) != 0)
{
	int c = 0;
}*
   这之后使用该套接字的accept和由accept产生的通信套接字将变成非阻塞模式,即不用等待有结果即可函数返回并执行下面的语句。

方式二:在Windows中使用MFC的函数WSAAsyncSelect来注册网络事件,这种方式需要将网络事件绑定到特定的窗口,而WSAEventSelect则不需要绑定到特定窗口。如下:

//注册网络事件
	if (SOCKET_ERROR == WSAAsyncSelect(m_socket, m_hWnd, UM_SOCK, FD_READ))  
	{
		  MessageBox(_T("网络注册事件失败!"));
		  return FALSE;
	}
    第二种方式中,函数WSAAsyncSelect放在哪个位置,可以根据注册的网络事件来,如在服务端,第一个WSAAsyncSelect应该是注册等待连接事件,所以应该放在监听之后,客户端则放在连接之后。
    在项目的服务端测试时采用了第一种方式,在项目的客户端采用了第二种方式,在项目的服务端采用了第二种方式。
    测试结果分析:第一种方式可以实现非阻塞,因为函数返回并不代表实现了该函数的作用,所以若之后的操作和该函数的结果有关,则应该加条件控制,否则这些操作将白执行,并且可能出现误操作;第二种方式达不到预期效果,比如无限发送和接收数据时,只收到开头一两次结果。奇怪的是,在调试模式下结果是正确的。造成这种现象的原因未明。

虽然这两种方式避免使用使用多线程,即具有减少创建和销毁线程时带来的内存上的开销,但会出现正如实验结果一样会出现达不到预期效果的现象。
2)多线程方式
accept函数在无客户端连接时、recv在接收到数据之前会阻塞其所在线程的运行,即该线程会一直停留在此函数处,直到这些函数成功返回时才会执行在该线程中此函数之后的语句。既然这些函数只是阻塞其所在的线程,那么可以将不必等待该函数成功返回时就执行的操作放在其它线程中执行。这利用多线程实现了异步socket。
我在项目的服务端程序中将accept放在AcceptThread线程中,每次accept成功后会创建一个DataThread线程,该线程用于接收来自客户端的数据和向客户端发送数据。注意,一个服务器可以和多个客户端连接并通信,但对于一个客户端来说,服务器程序在调用accept时只会成功返回一次,其余都是阻塞,除非有新的客户端与服务器相连。在客户端项目中,将send和recv分别放在SendThread线程和Recv线程中。
发送数据和接收数据是否放在同一线程中或者放在不同线程中是否需要加控制的考虑因素:数据的发送和接收之间的关联性。这种关联性有以下几个方面:①一来一回的关系:先接收数据再发送数据,这种多出现在服务端;先发送数据再接收数据,这种多出现在客户端。②多来一回或者一来多回。③无关联,即发送什么数据和接收什么数据无直接或间接联系。
针对以上几种关联性,对应的方案如下:
关联性①:同一线程方案:若是先接受数据再发送数据,则应该保证接收数据在前,发送数据在后,因为recv会阻塞,所以只要没能成功接收到数据,后面发送数据的代码自然不能执行;若是先发送数据再接收数据,只需按顺序编程发送和接收代码即可。不同线程方案:即发送和接收数据分别在两个线程中,先执行的线程先启动。这种方式需要加同步控制。
关联性②:同一线程或两个线程都可,同一线程则需要控制函数调用的条件,不同线程则需控制同步条件,如采用事件对象同步,则在满足次数之后才将对方的事件对象置为有信号。
关联性③:将发送数据和接收数据放在不同线程中。因为recv会阻塞,所以不能将其放在同一线程中。
3)测试时采用的几种方式
① accept放在主线程,accept之后创建数据发送和(或)接收线程的方式:在main程序中不会出现无响应现象,但在MFC程序中运行到accept时会出现无响应现象,不管是否在while循环中。*
② accept、数据发送和接收放在同一个线程中的方式:因为accept会阻塞,所以这种方式使得服务端和客户端只能发送和接收一次数据。可以加accept的调用的控制条件,定义一个bool型变量,在accept连接成功之前该变量为真,成功之后该变量为假,则只有在该变量为真时才调用accept,即有新的客户端发起连接,连接之后直到又一个客户端到来之前都不会调accept。在这种方式下,accept阻塞只会发生在无客户端与服务器连接或两者正在连接,未连接成功,理应阻塞。网上说的加锁,不知道是不是采用这种方式。
关于异步实现非阻塞的总结:只要知道哪些函数或地方会造成阻塞,就能在
相应的地方加控制,实现功能上的异步。至于效率和开销方面,可能有待考虑。
*
个人对多客户端同时与服务端通信时的异步处理的理解:在查阅资料时,经常看到这样的说法,就是在多客户端与服务器通信时,要注意加异步处理以防止阻塞,否则将可能没办法保证两者的正确通信。当时以为只有这种情况才需要加异步处理,后来发现即使是一客户端对一服务器,也要加异步处理。因为如果服务器与客户端需要多次通信时,若不加异步处理,accept会阻塞,只能实现一次通信。又或者只是一次通信,在MFC程序中,accept阻塞主线程会导致运行窗口无响应。这里的异步处理是广义的,包括多线程、控制处理等。*
5、阻塞函数变得非阻塞
在测试过程中发现一种奇怪的现象,即调用阻塞函数就算没有成功也会返回。这种情况并非是使用了非阻塞套接字或将套接字设置成了非阻塞模式。
recv在没收到数据也会返回,原因如下:
没有调用accept、connect函数,或者调用了但没等这些函数成功返回即调用recv函数;
一个服务器使用了另一个服务器正在使用的端口。这是书上说的情况,但我还没有实验过。
经验:注意调用一个函数后要判断其返回值,根据返回值的不同执行对应的操作。

6、小识
1)string转为const char*

string sendTest; 
sendTest= "服务器1";
char sendBuf[MaxBufSize] = {0};
strcpy(sendBuf, sendTest.c_str());
//发送数据
int sendResult = send(dlg->m_clientSocket, sendBuf, sizeof(sendBuf), 0); 

2)线程

 //第一次进入线程时才执行while之前的代码,之后再进入则只执行while内的代码
//若没有while循环,则线程只执行一次,因为执行到return时返回了,即结束了线程的执行
       UINT TestThread(LPVOID lpParam)
    {
    	   int test = 0;                 //第一次进入线程时执行
    	   while (TRUE)
    	   {                             
    		  test++;
    	    }
    	   return 0;
    }

3)MFC程序主线程中的无限循环
不要在一个按钮的消息响应函数中编写无限循环的代码,否则会出现程序无响应现象,但在线程中使用无限循环就不会出现这样的现象。

7、socket通信:java
1)服务端:
① 创建套接字:

 server = new ServerSocket(6000); 

② 等待连接:

 socket = server.accept();

③ 接收数据:输入流操作

     //接收客户端数据
     BufferedReader br = new BufferedReader(new 
InputStreamReader(socket.getInputStream(), "GBK"));//读客户端的数据并
设置编码格式为"GBK"
     StringBuilder sb = new StringBuilder();
     String temp;
     int index;
     while ((temp = br.readLine()) != null) {
     System.out.println(temp);
     if ((index = temp.indexOf("eof")) != -1) {//遇到eof时就结束接收
     sb.append(temp.substring(0, index));
     break;
     }
     sb.append(temp);
     }

④ 发送数据:输出流操作

    //向客户端发送数据
    Writer writer = new OutputStreamWriter(socket.getOutputStream(), "UTF-8");
   writer.write("Hello,Client");
   writer.write("eof\n");
   writer.flush();

2)客户端
① 创建并连接套接字(包括绑定到IP地址和端口号):

String host = "127.0.0.1";  //要连接的服务端IP地址,这里为了便于测试,选择本地IP
int port = 9000;   //要连接的服务端对应的监听端口,可任意选择
Socket client = new Socket(host, port);//与服务端建立连接

② 发送数据:输出流操作

Writer writer = new OutputStreamWriter(client.getOutputStream(), "GBK");  //建立连接后就可以往服务端写数据,设置数据格式为"GBK"
writer.write("Hello,Server.");//写入要传输给服务器的数据
writer.write("eof\n");//每次用eof作为结束标志符
writer.flush();//记住要flush一下,只有这样服务端才能收到客户端发送的数据,否则可能会引起两边无限的互相等待。

③ 接收数据:输入流操作

BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream(), "UTF-8"));    //读服务端的数据,并设置编码格式为"UTF-8"
int timeout = 10;
client.setSoTimeout(timeout * 1000);//Socket为我们提供了一个setSoTimeout()方法来设置接收数据的超时时间,单位是毫秒。
StringBuffer sb = new StringBuffer();//StringBuffer用来连接收到的字符串
String temp;//BufferedReader提供了缓存,使用readLine来一次读一行,提高了读的效率,temp用来记录每一行的内容
int index;//判断是否读到最后
try {
    while ((temp = br.readLine()) != null) {
    if ((index = temp.indexOf("eof")) != -1) {//若读到最后,记录下后就break
    sb.append(temp.substring(0, index));
    break;
     }
     sb.append(temp);//连接字符串
     }
    } catch (SocketTimeoutException e) {
      System.out.println("time out");//因为设置了超时时间,当超时时需抛出异常
    }
    System.out.println("server: " + sb);
    writer.close();//close的顺序要注意,必须从后往前关闭
    br.close();
    client.close();

3)异步
服务端的acept也是阻塞的,所以当有多个客户端与服务器相连或者服务器和客务端需要多次收发数据时,应通过异步方式实现非阻塞socket。
这里采用多线程方式实现异步socket,其基本思想和C++一样,都是在服务器调用accept和客户端连接成功时,启动新的线程与客户端通信。如下:
ServerSocket server = new ServerSocket(port);//创建一个端口号为9000的ServerSocket
while (true) {//服务端一直保持监听状态
Socket socket = server.accept();//server尝试接收其他Socket的连接请求,server的accept方法是阻塞式的
new Thread(new Task(socket)).start(); //每接收到一个Socket就建立一个新的线程,并启动该线程
}
8、socket通信:C++ VS java
由以上两者的服务器和客户端通信流程可以看出,java的socket通信比C++的要简便些,java的socket通信少了一些环节,猜想有可能是其调用的函数在其内部封装了一些操作,因为基于TCP/IP协议的socket通信的基本流程是一样的,都需要先创建套接字再连接套接字。但不同的语言、编译环境和操作系统,其具体的实现方式会不同。这就决定了在不同环境中编写的服务器和客户端在通信时,会存在因为API不一直导致的数据格式不统一。所以在这些服务器和客户端之间通信时,要注重发送的数据格式,否则虽然连接成功,但没法正确接收到数据。
下面是几种语言的socket通信:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/fazstyle/article/details/89139160