Multithreaded asynchronous socket communication based on TCP

Multithreaded asynchronous socket communication based on TCP

1. The socket process used by the server:
1) Load the socket library: WSAStartup (specific function descriptions will be given later)

//加载套接字库
	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) Version number detection:

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

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

3) Create a socket: socket

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

4) Bind the socket:

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

5) Listening socket: The
listen function uses an active connection socket to become a connected socket, so that a process can accept requests from other processes and become a server process. In TCP server programming, the listen function turns the process into a server and specifies the corresponding socket to become a passive connection. This function will block.

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

6) Waiting for client connection: accept

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

7) Receive client data: recv This function will block.

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

8) Send data to the client: send

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

2. The client uses the socket process
1) Load the socket library:

 //加载套接字库
	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) Version number detection:

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

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

3) Create a socket:

 //创建重叠套接字
	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) Connect the socket:

//Send a connection request to the server
if (SOCKET_ERROR== connect(m_socket, (SOCKADDR*)&m_sockAddr, sizeof(SOCKADDR)))
{ MessageBox(_T("Connection failed!")); return FALSE; }


5) Send socket library:

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

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

3. Communication diagram between server and client

Insert picture description here

4. Multithreaded asynchronous socket communication
CSDN a blogger's understanding of asynchronous (Async), synchronous (Sync), blocking (Block), non-blocking (Unblock): the asynchronous method means that the sender does not wait for the receiver to respond, and then continues The communication method of sending the next data packet; and synchronization refers to the communication method of sending the next data packet after receiving the response from the receiver after sending the data. Blocking socket means that when the network call of this socket is executed, it will not return until it succeeds, otherwise it has been blocked on this network call, such as calling the recv() function to read the data in the network buffer. If no data arrives, Will always hang on the recv() function call, until some data is read, this function call will return; non-blocking socket means that when the network call of this socket is executed, it will return immediately regardless of whether the execution is successful or not. . For example, the recv() function is called to read the data in the network buffer, and it returns immediately regardless of whether the data is read or not, instead of hanging on this function call all the time.
Personal understanding: blocking and non-blocking are phenomena, and asynchronous and synchronous are the means to produce this phenomenon.
1) There are two ways to set the socket to non-blocking:
Method 1: After creating the socket, call the function ioctlsocket to force it to be defined as non-blocking mode, as follows:

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

Method 2: Use the MFC function WSAAsyncSelect to register network events in Windows. This method requires binding network events to a specific window, while WSAEventSelect does not need to be bound to a specific window. as follows:

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

Although these two methods avoid the use of multithreading, that is, they reduce the memory overhead caused by creating and destroying threads, but they will not achieve the expected results just like the experimental results.
2) In multithreaded mode, the
accept function will block the running of its thread before receiving data when there is no client connection, that is, the thread will stay in this function until these functions return successfully. The statement after this function in the thread. Since these functions only block the thread in which they are located, operations that do not need to wait for the function to return successfully can be executed in other threads. This uses multithreading to implement asynchronous sockets.
I put accept in the AcceptThread thread in the server program of the project. Each time the accept succeeds, a DataThread thread is created. This thread is used to receive data from the client and send data to the client. Note that a server can connect and communicate with multiple clients, but for a client, the server program will only successfully return once when calling accept, and the rest are blocked unless a new client is connected to the server. In the client project, put send and recv in SendThread thread and Recv thread respectively.
Whether sending data and receiving data are placed in the same thread or in different threads needs to be controlled: the correlation between the sending and receiving of data. This kind of correlation has the following aspects: ① One-to-one relationship: receiving data first and then sending data, which mostly occurs on the server; sending data first and then receiving data, which mostly occurs on the client. ② One more time or one more time. ③ No connection, that is, there is no direct or indirect connection between what data is sent and what data is received.
In view of the above relevance, the corresponding solutions are as follows:
Relevance ①: The same thread scheme: If you receive data first and then send data, you should ensure that the data is received before the data is sent, because recv will block, so as long as the data is not successfully received, the code that sends the data later cannot naturally Execute; if you send data first and then receive data, you only need to program the sending and receiving codes in sequence. Different thread schemes: Send and receive data in two threads respectively, and the thread that executes first starts first. This method requires synchronization control.
Correlation ②: The same thread or two threads can be used, the same thread needs to control the condition of function call, and different threads need to control the synchronization condition. If the event object synchronization is adopted, the other party's event object will be set to There is a signal.
Relevance ③: Put sending data and receiving data in different threads. Because recv will block, it cannot be placed in the same thread.
3) Several methods used in the test
① Accept is placed in the main thread, and the data sending and (or) receiving threads are created after accept: There will be no unresponsiveness in the main program, but when it runs to accept in the MFC program There will be no response, regardless of whether it is in the while loop. *
② The way that accept, data sending and receiving are placed in the same thread: Because accept will block, this way makes the server and client only send and receive data once. You can add the control condition of accept call, define a bool type variable, the variable is true before accept connection is successful, and the variable is false after success, then accept is called only when the variable is true, that is, there is a new client initiates a connection, the connection until after another before the arrival of the client will not be transferred by accept. In this way, accept blocking only occurs when no client is connected to the server or both are connecting. If the connection is not successful, it should be blocked. I don’t know if this method is used for locking.
Summary of non-blocking asynchronous implementation: as long as you know which functions or places will cause blocking, you can
Add control to the corresponding place to achieve functional asynchronous. As for efficiency and cost, it may need to be considered.
*
Personal understanding of asynchronous processing when multiple clients communicate with the server at the same time: When looking up data, I often see this statement, that is, when multiple clients communicate with the server, pay attention to asynchronous processing to prevent blocking, otherwise There may be no way to ensure correct communication between the two. At that time, I thought that asynchronous processing was only needed in this case, but later discovered that even if it is a client to a server, asynchronous processing should be added. Because if the server and the client need to communicate multiple times, if asynchronous processing is not added, accept will block and only one communication can be achieved. Or it is just a communication. In an MFC program, accept blocking the main thread will cause the running window to become unresponsive. Asynchronous processing here is broad, including multithreading, control processing, etc. *
5. Blocking function becomes non-blocking
. A strange phenomenon was discovered during the test, that is, calling the blocking function will return even if it fails. This situation is not to use a non-blocking socket or set the socket to non-blocking mode.
Recv will return even if it does not receive data. The reasons are as follows: the
accept and connect functions are not called, or the recv function is called without waiting for these functions to return successfully;
one server uses the port that another server is using. This is what the book says, but I haven't experimented yet.
Experience: Pay attention to determine the return value of a function after calling it, and perform the corresponding operation according to the return value.

6, small knowledge
1) string to 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) Thread

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

3) Infinite loop
in the main thread of the MFC program Do not write an infinite loop code in the message response function of a button, otherwise the program will not respond, but using infinite loop in the thread will not appear such a phenomenon.

7. Socket communication: java
1) Server:
① Create socket:

 server = new ServerSocket(6000); 

② Waiting for connection:

 socket = server.accept();

③ Receive data: input stream operation

     //接收客户端数据
     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);
     }

④ Send data: output stream operation

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

2) Client
① Create and connect socket (including binding to IP address and port number):

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

② Send data: output stream operation

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

③ Receive data: input stream operation

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)
The acept of the asynchronous server is also blocking, so when there are multiple clients connected to the server or the server and the client need to send and receive data multiple times, non-blocking sockets should be implemented asynchronously.
Here, the asynchronous socket is implemented in a multithreaded manner. The basic idea is the same as that of C++. When the server calls accept and the client connects successfully, a new thread is started to communicate with the client. As follows:
ServerSocket server = new ServerSocket(port);//Create a ServerSocket with a port number of 9000
while (true) {//The server keeps listening
Socket socket = server.accept();//The server tries to receive other Socket For connection requests, the server's accept method is the blocking
new Thread(new Task(socket)).start(); //A new thread is created every time a Socket is received, and the thread is started
}
8. Socket communication: C++ VS java
can be seen from the server and client communication process of the above two, the socket communication of java is simpler than that of C++, and the socket communication of java has fewer links. I guess it may be that the function it calls is encapsulated inside it. Some operations, because the basic process of socket communication based on the TCP/IP protocol is the same, you need to create a socket first and then connect the socket. However, different languages, compilation environments, and operating systems have different specific implementation methods. This determines that when the server and the client written in different environments communicate, there will be inconsistent data formats due to the inconsistent API. Therefore, when communicating between these servers and clients, pay attention to the data format sent, otherwise, although the connection is successful, the data cannot be received correctly.
The following is the socket communication in several languages:
Insert picture description here

Guess you like

Origin blog.csdn.net/fazstyle/article/details/89139160