網絡通信基礎重難點解析 02:TCP 通信基本流程

TCP 通信基本流程

不管多麼複雜的服務器或客戶端程序,其網絡通信的基本原理一定如下所述:

對於服務器,其通信流程一般有如下步驟:

1. 調用 socket 函數創建 socket(偵聽socket)
2. 調用 bind 函數 將 socket綁定到某個ip和端口的二元組上
3. 調用 listen 函數 開啓偵聽
4. 當有客戶端請求連接上來後,調用 accept 函數接受連接,產生一個新的 socket(客戶端 socket)
5. 基於新產生的 socket 調用 send 或 recv 函數開始與客戶端進行數據交流
6. 通信結束後,調用 close 函數關閉偵聽 socket

對於客戶端,其通信流程一般有如下步驟:

1. 調用 socket函數創建客戶端 socket
2. 調用 connect 函數嘗試連接服務器
3. 連接成功以後調用 send 或 recv 函數開始與服務器進行數據交流
4. 通信結束後,調用 close 函數關閉偵聽socket

上述流程可以繪製成如下圖示:

在這裏插入圖片描述

對於上面的圖,讀者可能有疑問,爲什麼客戶端調用 close() ,會和服務器端 recv() 函數有關。這個涉及到 recv() 函數的返回值意義,我們在下文中詳細講解。

服務器端實現代碼:

/**
 * TCP服務器通信基本流程
 * zhangyl 2018.12.13
 */
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

int main(int argc, char* argv[])
{
    //1.創建一個偵聽socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1)
    {
        std::cout << "create listen socket error." << std::endl;
        return -1;
    }

    //2.初始化服務器地址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
    {
        std::cout << "bind listen socket error." << std::endl;
        return -1;
    }

	//3.啓動偵聽
    if (listen(listenfd, SOMAXCONN) == -1)
    {
        std::cout << "listen error." << std::endl;
        return -1;
    }

    while (true)
    {
        struct sockaddr_in clientaddr;
        socklen_t clientaddrlen = sizeof(clientaddr);
		//4. 接受客戶端連接
        int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
        if (clientfd != -1)
        {         	
			char recvBuf[32] = {0};
			//5. 從客戶端接受數據
			int ret = recv(clientfd, recvBuf, 32, 0);
			if (ret > 0) 
			{
				std::cout << "recv data from client, data: " << recvBuf << std::endl;
				//6. 將收到的數據原封不動地發給客戶端
				ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
				if (ret != strlen(recvBuf))
					std::cout << "send data error." << std::endl;
				
				std::cout << "send data to client successfully, data: " << recvBuf << std::endl;
			} 
			else 
			{
				std::cout << "recv data error." << std::endl;
			}
			
			close(clientfd);
        }
    }
	
	//7.關閉偵聽socket
	close(listenfd);

    return 0;
}

客戶端實現代碼:

/**
 * TCP客戶端通信基本流程
 * zhangyl 2018.12.13
 */
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT     3000
#define SEND_DATA       "helloworld"

int main(int argc, char* argv[])
{
    //1.創建一個socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1)
    {
        std::cout << "create client socket error." << std::endl;
        return -1;
    }

    //2.連接服務器
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
    serveraddr.sin_port = htons(SERVER_PORT);
    if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
    {
        std::cout << "connect socket error." << std::endl;
        return -1;
    }

	//3. 向服務器發送數據
	int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
	if (ret != strlen(SEND_DATA))
	{
		std::cout << "send data error." << std::endl;
		return -1;
	}
	
	std::cout << "send data successfully, data: " << SEND_DATA << std::endl;
	
	//4. 從客戶端收取數據
	char recvBuf[32] = {0};
	ret = recv(clientfd, recvBuf, 32, 0);
	if (ret > 0) 
	{
		std::cout << "recv data successfully, data: " << recvBuf << std::endl;
	} 
	else 
	{
		std::cout << "recv data error, data: " << recvBuf << std::endl;
	}
	
	//5. 關閉socket
	close(clientfd);

    return 0;
}

以上代碼,服務器端在地址 0.0.0.0:3000 啓動一個偵聽,客戶端連接服務器成功後,給服務器發送字符串"helloworld";服務器收到後,將收到的字符串原封不動地發給客戶端。

在 Linux Shell 界面輸入以下命令編譯服務器端和客戶端:

# 編譯 server.cpp 生成可執行文件 server   
[root@localhost testsocket]# g++ -g -o server server.cpp
# 編譯 client.cpp 生成可執行文件 client
[root@localhost testsocket]# g++ -g -o client client.cpp

接着,我們看下執行效果,先啓動服務器程序:

[root@localhost testsocket]# ./server

再啓動客戶端程序:

[root@localhost testsocket]# ./client 

這個時候客戶端輸出:

send data successfully, data: helloworld
recv data successfully, data: helloworld

服務器端輸出:

recv data from client, data: helloworld
send data to client successfully, data: helloworld

以上就是 TCP socket 網絡通信的基本原理,對於很多讀者來說,上述代碼可能很簡單,更有點“玩具”的意味。但是深刻理解這兩個代碼片段是進一步學習開發複雜的網絡通信程序的基礎。而且看似很簡單的代碼,卻隱藏了很多的玄機和原理,接下來的章節我們將以這兩段代碼爲藍本,逐漸深入。


本文首發於『easyserverdev』公衆號,歡迎關注,轉載請保留版權信息。

歡迎加入高性能服務器開發 QQ 羣一起交流: 578019391
微信掃碼關注

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章