Windows/Linux TCP Socket网络编程简介及测试代码
典型的網絡應用是由一對程序(即客戶程序和服務器程序)組成的,它們位于兩個不同的端系統中。當運行這兩個程序時,創建了一個客戶進程和一個服務器進程,同時它們通過從套接字(socket)讀出和寫入數據在彼此之間進行通信。開發者創建一個網絡應用時,其主要任務就是編寫客戶程序和服務器程序的代碼。
網絡應用程序有兩類。一類是由協議標準(如一個RFC或某種其它標準文檔)中所定義的操作的實現,這樣的應用程序有時稱為”開放”的,因為定義其操作的這些規則為人們所共知。對于這樣的實現,客戶程序和服務器程序必須遵守由該RFC所規定的規則。另一類網絡應用程序是專用的網絡應用程序。在這種情況下,由客戶和服務器程序應用的應用層協議沒有公開發布在某RFC中或其它地方。某單獨的開發者(或開發團隊)產生了客戶和服務器程序,并且該開發者用他的代碼完全控制該代碼的功能。但是因為這些代碼并沒有實現一個開放的協議,其它獨立的開發者將不能開發出和該應用程序交互的代碼。
RFC(Request for Comments):請求意見稿,是由互聯網工程任務組(IETF)發布的一系列備忘錄。文件收集了有關互聯網相關信息,以及UNIX和互聯網社群的軟件文件,以編號排定。目前RFC文件是由互聯網協會(ISOC)贊助發行。基本的互聯網通信協議都有在RFC文件內詳細說明。RFC已經成為IETF、Internet Architecture Board(IAB)還有其他一些主要的公共網絡研究社區的正式出版物發布途徑。RFC文件只有新增,不會有取消或中途停止發行的情形。但是對于同一主題而言,新的RFC文件可以聲明取代舊的RFC文件。
TCP網絡編程有兩種模式:一種是服務器模式,另一種是客戶端模式。服務器模式創建一個服務程序,等待客戶端用戶的連接,接收到用戶的連接請求后,根據用戶的請求進行處理;客戶端模式則根據目的服務器的地址和端口進行連接,向服務器發送請求并對服務器的響應進行數據處理。
TCP是面向連接的,并且為兩個端系統之間的數據流動提供可靠的字節流通道。UDP是無連接的,從一個端系統向另一個端系統發送獨立的數據分組,不對交付提供任何保證。當客戶或服務器程序實現了一個由RFC定義的協議時,它應當使用與該協議關聯的周知端口號;與之相反,當研發一個專用應用程序時,研發者必須注意避免使用這些周知端口號。
與UDP不同,TCP是一個面向連接的協議。這意味著在客戶和服務器能夠開始互相發送數據之前,它們先要握手和創建一個TCP連接。TCP連接的一端與客戶套接字相聯系,另一端與服務器套接字相聯系。當創建該TCP連接時,我們將其與客戶套接字地址(IP地址和端口號)和服務器套接字地址(IP地址和端口號)關聯起來。使用創建的TCP連接,當一側要向另一側發送數據時,它只需經過其套接字將數據丟進TCP連接。這與UDP不同,UDP服務器在將分組丟進套接字之前必須為其附上一個目的地地址。
TCP中客戶程序和服務器程序的交互:客戶具有向服務器發起接觸的任務。服務器為了能夠對客戶的初始接觸做出反應,服務器必須已經準備好。這意味著兩件事:第一,TCP服務器在客戶試圖發起接觸前必須作為進程運行起來。第二,服務器程序必須具有一扇特殊的門,更精確地說是一個特殊的套接字,該門歡迎來自運行在任意主機上的客戶進程的某種初始接觸。有時我們將客戶的初始接觸稱為”敲歡迎之門”。隨著服務器進程的運行,客戶進程能夠向服務器發起一個TCP連接。這是由客戶程序通過創建一個TCP套接字完成的。當該客戶生成其TCP套接字時,它指定了服務器中的歡迎套接字的地址,即服務器主機的IP地址及其套接字的端口號。生成其套接字后,該客戶發起了一個三次握手并創建與服務器的一個TCP連接。發生在運輸層的三次握手,對于客戶和服務器程序是完全透明的。
在三次握手期間,客戶進程敲服務器進程的歡迎之門。當該服務器”聽”到敲門聲時,它將生成一扇新門(更精確地講是一個新套接字),它專門用于特定的客戶。它是專門對客戶進行連接的新生成的套接字,稱為連接套接字。從應用程序的觀點來看,客戶套接字和服務器連接套接字直接通過一根管道連接,如下圖所示:客戶進程可以向它的套接字發送任意字節,并且TCP保證服務器進程能夠按發送的順序接收(通過連接套接字)每個字節。TCP因此在客戶和服務器進程之間提供了可靠服務。此外,客戶進程不僅能向它的套接字發送字節,也能從中接收字節;類似地,服務器進程不僅從它的連接套接字接收字節,也能向其發送字節。
創建客戶套接字時未指定其端口號;相反,我們讓操作系統為我們做此事。connect方法執行完后,執行三次握手,并在客戶和服務器之間創建起一條TCP連接。在服務器調用accept方法后,客戶和服務器則完成了握手。客戶和服務器之間創建了一個TCP連接。借助于創建的TCP連接,客戶與服務器現在能夠通過該連接相互發送字節。使用TCP,從一側發送的所有字節不僅能確保到達另一側,而且確保按序到達。
TCP和UDP協議是以IP協議為基礎的傳輸,為了方便多種應用程序,區分不同應用程序的數據和狀態,引入了端口的概念。端口號是一個16比特的數,其大小在0~65535之間,通常稱這個值為端口號。0~1023范圍的端口號稱為周知端口號(well-known port number),是受限制的,這是指它們保留給諸如HTTP(它使用端口號80)和FTP(它使用端口號21)之類的周知應用層協議來使用。當我們開發一個新的應用程序時,必須為其分配一個端口號。如果應用程序開發者所編寫的代碼實現的是一個”周知協議”的服務器端,那么開發者就必須為其分配一個相應的周知端口號。通常,應用程序的客戶端讓運輸層自動地(并且是透明地)分配端口號,而服務器端則分配一個特定的端口號。如果是服務程序,則需要對某個端口進行綁定,這樣某個客戶端可以訪問本主機上的此端口來與應用程序進行通信。由于IP地址只能對主機進行區分,而加上端口號就可以區分此主機上的應用程序。實際上,IP地址和端口號的組合,可以確定在網絡上的一個程序通路,端口號實際上是操作系統標識應用程序的一種方法。
端口號的值可由用戶自定義或者由系統分配,采用動態系統分配和靜態用戶自定義相結合的辦法。一些常用的服務程序使用固定的靜態端口號,例如,Web服務器的端口號為80,電子郵件SMTP的端口號為25,文件傳輸FTP的端口號為20和21等。對于其他的應用程序,特別是用戶自行開發的客戶端應用程序,端口號采用動態分配方法,其端口號由操作系統自動分配。通常情況下,對端口的使用有如下約定,小于1024的端口未保留端口,由系統的標準服務程序使用;1024以上的端口號,用戶應用程序可以使用。
套接字有三種類型:流式套接字(SOCK_STREAM)、數據報套接字(SOCK_DGRAM)及原始套接字。
(1).流式套接字:可以提供可靠的、面向連接的通訊流。如果你通過流式套接字發送了順序的數據:”1”、”2”,那么數據到達遠程時候的順序也是”1”、”2”。Telnet是流式連接。還有WWW瀏覽器,它使用的HTTP協議也是通過流式套接字來獲取網頁的。流式套接字使用了TCP(The Transmission Control Protocol)協議。TCP保證了你的數據傳輸是正確的,并且是順序的。
(2).數據報套接字:定義了一種無連接的服務,數據通過相互獨立的報文進行傳輸,是無序的,并且不保證可靠,無差錯。它使用數據報協議UDP(User Datagram Protocol)。UDP不像流式套接字那樣維護一個打開的連接,你只需要把數據打成一個包,把遠程的IP貼上去,然后把這個包發送出去。這個過程是不需要建立連接的。UDP的應用例子有:tfpt, bootp等。
(3).原始套接字:主要用于一些協議的開發,可以進行比較底層的操作。
流式套接字工作過程:服務器首先啟動,通過調用socket建立一個套接字,然后調用bind將該套接字和本地網絡地址聯系在一起,再調用listen使套接字做好偵聽的準備,并規定它的請求隊列的長度,之后就調用accept來接收連接。客戶在建立套接字后就可調用connect和服務器建立連接。連接一旦建立,客戶機和服務器之間就可以通過調用recv和send來接收和發送數據。最后,待數據傳送結束后,雙方調用close關閉套接字。
網絡字節序是指多字節變量在網絡傳輸時的表示方法,網絡字節序采用大端字節序的表示方法。這樣小端字節序的系統通過網絡傳輸變量的時候需要進行字節序的轉換,大端字節序的變量則不需要進行轉換。字節序是由于不同的主處理器和操作系統,對大于一個字節的變量在內存中的存放順序不同而產生的。
小端字節序(Little Endian, LE):在表示變量的內存地址的起始地址存放低字節,高字節順序存放。LE主要用于我們現在的PC的CPU中,即Intel的x86系列兼容機。
大端字節序(Bit Endian, BE):在表示變量的內存地址的起始地址存放高字節,低字節順序存放。
注:以上內容主要摘自于:《計算機網絡自頂向下方法(原書第7版)》、《Linux網絡編程(第2版)》
以下是測試代碼段,可同時在Windows和Linux下執行,并對代碼中用到的函數進行了說明:
const char* server_ip_ = "10.4.96.33"; // 服務器ip
const int server_port_ = 8888; // 服務器端口號,需確保此端口未被占用// linux: $ netstat -nap | grep 6666; kill -9 PID// windows: tasklist | findstr OpenSSL_Test.exe; taskkill /T /F /PID PID
const int server_listen_queue_length_ = 100; // 服務器listen隊列支持的最大長度
以上代碼段是設置的三個全局常量,server_ip_是指定測試的服務器端ip,server_port為指定的服務器端端口號,server_listen_queue_length_為指定服務器端listen隊列支持的最大長度。
#ifdef _MSC_VER
// 每一個WinSock應用程序必須在開始操作前初始化WinSock的動態鏈接庫(DLL),并在操作完成后通知DLL進行清除操作
class WinSockInit {
public:WinSockInit(){WSADATA wsaData;// WinSock應用程序在開始時必須要調用WSAStartup函數,結束時調用WSACleanup函數// WSAStartup函數必須是WinSock應用程序調用的第一個WinSock函數,否則,其它的WinSock API函數都將會失敗并返回錯誤值int ret = WSAStartup(MAKEWORD(2, 2), &wsaData);if (ret != NO_ERROR)fprintf(stderr, "fail to init winsock: %d\n", ret);}~WinSockInit(){WSACleanup();}
};static WinSockInit win_sock_init_;#define close(fd) closesocket(fd)
#define socklen_t int
#else
#define SOCKET int
#endif
以上代碼段主要是實現了WinSockInit類,此類僅在Windows下使用,用于WinSock的初始化。
int get_error_code()
{
#ifdef _MSC_VERauto err_code = WSAGetLastError();
#elseauto err_code = errno;
#endifreturn err_code;
}
get_error_code函數是為了獲取調用相關函數時返回的錯誤碼,在linux使用errno,windows上不支持errno,需要使用WSAGetLastError函數。
// 服務器端處理來自客戶端的數據
void calc_string_length(SOCKET fd)
{// 從客戶端接收數據const int length_recv_buf = 2048;char buf_recv[length_recv_buf];std::vector<char> recved_data;//std::this_thread::sleep_for(std::chrono::seconds(10)); // 為了驗證客戶端write或send會超時while (1) {auto num = recv(fd, buf_recv, length_recv_buf, 0);if (num <= 0) {auto err_code = get_error_code();if (num < 0 && err_code == EINTR) {continue;}std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to recv: %d, error code: %d, message: %s\n", num, err_code, ec.message().c_str());close(fd);return;}bool flag = false;std::for_each(buf_recv, buf_recv + num, [&flag, &recved_data](const char& c) {if (c == '\0') flag = true; // 以空字符作為接收結束的標志else recved_data.emplace_back(c);});if (flag == true) break;}fprintf(stdout, "recved data: ", recved_data.data());std::for_each(recved_data.data(), recved_data.data() + recved_data.size(), [](const char& c){fprintf(stdout, "%c", c);});fprintf(stdout, "\n");// 向客戶端發送數據auto str = std::to_string(recved_data.size());std::vector<char> vec(str.size() + 1);memcpy(vec.data(), str.data(), str.size());vec[str.size()] = '\0';const char* ptr = vec.data();auto left_send = str.size() + 1; // 以空字符作為發送結束的標志//std::this_thread::sleep_for(std::chrono::seconds(10)); // 為了驗證客戶端read或recv會超時while (left_send > 0) {auto sended_length = send(fd, ptr, left_send, 0); // writeif (sended_length <= 0) {int err_code = get_error_code();if (sended_length < 0 && err_code == EINTR) {continue;}std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to send: %d, error code: %d, message: %s\n", sended_length, err_code, ec.message().c_str());close(fd);return;}left_send -= sended_length;ptr += sended_length;}close(fd);
}
以上代碼段是服務器端調用的函數,服務器端程序會為每個連接上的客戶程序創建一個線程,調用此函數來進行服務器端和客戶端的數據的接收和發送處理。服務器端接收來自客戶端的數據,并計算其長度,然后將其長度發送給客戶端。
// 設置套接字為非阻塞的
int set_client_socket_nonblock(SOCKET fd)
{
#ifdef _MSC_VERu_long n = 1;// ioctlsocket: 通過將第2個參數設置為FIONBIO變更套接字fd的操作模式// 當此函數的第3個參數為true時,變更為非阻塞模式;為false時,變更為阻塞模式auto ret = ioctlsocket(fd, FIONBIO, &n);if (ret != 0) {fprintf(stderr, "fail to ioctlsocket: %d\n", ret);return -1;}
#else// fcntl: 向打開的套接字fd發送命令,更改其屬性; F_GETFL/F_SETFL: 獲得/設置套接字fd狀態值; O_NONBLOCK: 設置套接字為非阻塞模式auto ret = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);if (ret < 0) {fprintf(stderr, "fail to fcntl: %d\n", ret);}
#endifreturn 0;
}// 設置套接字為阻塞的
int set_client_socket_block(SOCKET fd)
{
#ifdef _MSC_VERu_long n = 0;auto ret = ioctlsocket(fd, FIONBIO, &n);if (ret != 0) {fprintf(stderr, "fail to ioctlsocket: %d\n", ret);return -1;}
#elseauto ret = fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) & ~O_NONBLOCK);if (ret < 0) {fprintf(stderr, "fail to fcntl: %d\n", ret);}
#endifreturn 0;
}// 設置連接超時
int set_client_connect_time_out(SOCKET fd, const sockaddr* server_addr, socklen_t length, int seconds)
{
#ifdef _MSC_VERif (seconds <= 0) {
#elseif (fd >= FD_SETSIZE || seconds <= 0) {
#endifreturn connect(fd, server_addr, length);}set_client_socket_nonblock(fd);auto ret = connect(fd, server_addr, length);if (ret == 0) {set_client_socket_block(fd);fprintf(stdout, "non block connect return 0\n");return 0;}
#ifdef _MSC_VERelse if (ret == SOCKET_ERROR && WSAGetLastError() != WSAEWOULDBLOCK) {
#elseelse if (ret < 0 && errno != EINPROGRESS) {
#endiffprintf(stderr, "non block connect fail return: %d\n", ret);return -1;}// 設置超時fd_set fdset;FD_ZERO(&fdset);FD_SET(fd, &fdset);struct timeval tv;tv.tv_sec = seconds;tv.tv_usec = 0;// select: 非阻塞方式,返回值:0:表示超時; 1:表示連接成功; -1:表示有錯誤發生// 注:在windows下select函數不作為計時器,在windows下,select的第一個參數可以忽略,可以是任意值ret = select(fd + 1, nullptr, &fdset, nullptr, &tv);if (ret < 0) {fprintf(stderr, "fail to select: %d\n", ret);return -1;} else if (ret == 0) {auto err_code = get_error_code();std::error_code ec(err_code, std::system_category());fprintf(stderr, "connect time out: error code: %d, message: %s\n", fd, err_code, ec.message().c_str());return -1;} else {int optval;socklen_t optlen = sizeof(optval);
#ifdef _MSC_VERret = getsockopt(fd, SOL_SOCKET, SO_ERROR, (char*)&optval, &optlen);
#else// getsockopt: 獲得套接字選項設置情況,此函數的第3個參數SO_ERROR表示獲取錯誤ret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &optval, &optlen);
#endifif (ret == -1 || optval != 0) {fprintf(stderr, "fail to getsockopt\n");return -1;}if (optval == 0) {set_client_socket_block(fd);fprintf(stdout, "connect did not time out\n");return 0;}}return 0;
}
以上代碼段是用來設置客戶端連接超時,通過調用select函數作為計時器,默認的connect、accept、recv、send函數都是屬于阻塞方式,而select是非阻塞方式。select一般作為計時器僅用于非Windows平臺,因為select的第一個參數套接字在windows上不起作用。
int set_client_send_time_out(SOCKET fd, int seconds)
{if (seconds <= 0) {fprintf(stderr, "seconds should be greater than 0: %d\n", seconds);return -1;}#ifdef _MSC_VERDWORD timeout = seconds * 1000; // millisecondsauto ret = setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout, sizeof(timeout));
#elsestruct timeval timeout;timeout.tv_sec = seconds;timeout.tv_usec = 0;// setsockopt: 設置套接字選項,為了操作套接字層的選項,此函數的第2個參數的值需指定為SOL_SOCKET,第3個參數SO_SNDTIMEO表示發送超時,第4個參數指定超時時間// 默認情況下send函數在發送數據的時候是不會超時的,當沒有數據的時候會永遠阻塞auto ret = setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));
#endifif (ret < 0) {fprintf(stderr, "fail to setsockopt: send\n");return -1;}return 0;
}// 設置接收數據recv超時
int set_client_recv_time_out(SOCKET fd, int seconds)
{if (seconds <= 0) {fprintf(stderr, "seconds should be greater than 0: %d\n", seconds);return -1;}#ifdef _MSC_VERDWORD timeout = seconds * 1000; // millisecondsauto ret = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
#elsestruct timeval timeout;timeout.tv_sec = seconds;timeout.tv_usec = 0;// setsockopt: 此函數的第3個參數SO_RCVTIMEO表示接收超時,第4個參數指定超時時間// 默認情況下recv函數在接收數據的時候是不會超時的,當沒有數據的時候會永遠阻塞auto ret = setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
#endifif (ret < 0) {fprintf(stderr, "fail to setsockopt: recv\n");return -1;}return 0;
}
以上代碼段是用來設置客戶端接收和發送數據超時,主要通過setsockopt函數實現。
int test_socket_tcp_client()
{// 1.創建流式套接字auto fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {auto err_code = get_error_code();std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to socket: %d, error code: %d, message: %s\n", fd, err_code, ec.message().c_str());return -1;}struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(server_port_);auto ret = inet_pton(AF_INET, server_ip_, &server_addr.sin_addr);if (ret != 1) {fprintf(stderr, "fail to inet_pton: %d\n", ret);return -1;}set_client_send_time_out(fd, 2); // 設置write或send超時時間set_client_recv_time_out(fd, 2); // 設置read或recv超時時間// 2.連接// connect函數的第二參數是一個指向數據結構sockaddr的指針,其中包括客戶端需要連接的服務器的目的端口和IP地址,以及協議類型ret = set_client_connect_time_out(fd, (struct sockaddr*)&server_addr, sizeof(server_addr), 2); // 設置連接超時時間//ret = connect(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));if (ret != 0) {auto err_code = get_error_code();std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to connect: %d, error code: %d, message: %s\n", ret, err_code, ec.message().c_str());return -1;}// 3.接收和發送數據// 向服務器端發送數據const char* buf_send = "https://blog.csdn.net/fengbingchun";const char* ptr = buf_send;auto length = strlen(buf_send);auto left_send = length + 1; // 以空字符作為發送結束的標志// 以下注釋掉的code僅用于測試write或send超時時間//std::unique_ptr<char> buf_send(new char[1024 * 1024]);//int length = 1024 * 1024;//long long count = 0;//for (;;) {//int left_send = length + 1;//const char* ptr = buf_send.get();//fprintf(stdout, "count: %lld\n", ++count);while (left_send > 0) {// send: 將緩沖區ptr中大小為left_send的數據,通過套接字文件描述符fd按照第4個參數flags指定的方式發送出去// send的返回值是成功發送的字節數.由于用戶緩沖區ptr中的數據在通過send函數進行發送的時候,并不一定能夠// 全部發送出去,所以要檢查send函數的返回值,按照與計劃發送的字節長度left_send是否相等來判斷如何進行下一步操作// 當send的返回值小于left_send的時候,表明緩沖區中仍然有部分數據沒有成功發送,這是需要重新發送剩余部分的數據// send發生錯誤的時候返回值為-1// 注意:send的成功返回并不一定意味著數據已經送到了網絡中,只說明協議棧有足夠的空間緩存數據,協議棧可能會為了遵循協議的約定推遲傳輸auto sended_length = send(fd, ptr, left_send, 0); // writeif (sended_length <= 0) {auto err_code = get_error_code();if (sended_length < 0 && err_code == EINTR) {continue;}std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to send: %d, err code: %d, message: %s\n", sended_length, err_code, ec.message().c_str());return -1;}left_send -= sended_length;ptr += sended_length;}//}// 從服務器端接收數據const int length_recv_buf = 2048;char buf_recv[length_recv_buf];std::vector<char> recved_data;while (1) {// recv: 用于接收數據,從套接字fd中接收數據放到緩沖區buf_recv中,第4個參數用于設置接收數據的方式// recv的返回值是成功接收到的字節數,當返回值為-1時錯誤發生auto num = recv(fd, buf_recv, length_recv_buf, 0); // readif (num <= 0) {auto err_code = get_error_code();if (num < 0 && err_code == EINTR) {continue;}std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to recv: %d, err code: %d, message: %s\n", num, err_code, ec.message().c_str());return -1;}bool flag = false;std::for_each(buf_recv, buf_recv + num, [&flag, &recved_data](const char& c) {if (c == '\0') flag = true; // 以空字符作為接收結束的標志else recved_data.emplace_back(c);});if (flag == true) break;}// 4.關閉套接字close(fd);// 驗證接收的數據是否是預期的fprintf(stdout, "send data: %s\n", buf_send, recved_data.data());fprintf(stdout, "recved data: ");std::for_each(recved_data.data(), recved_data.data() + recved_data.size(), [](const char& c){fprintf(stdout, "%c", c);});fprintf(stdout, "\n");std::string str(recved_data.data());auto length2 = std::stoi(str);if (length != length2) {fprintf(stderr, "received data is wrong: %d, %d\n", length, length2);return -1;}return 0;
}
以上代碼段是客戶端程序實現,同時支持在Windows和Linux上運行。
int test_socket_tcp_server()
{// 1.創建流式套接字// socket:參數依次為協議族、協議類型、協議編號. AF_INET: 以太網;// SOCK_STREAM:流式套接字,TCP連接,提供序列化的、可靠的、雙向連接的字節流auto fd = socket(AF_INET, SOCK_STREAM, 0);if (fd < 0) {auto err_code = get_error_code();std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to socket: %d, error code: %d, message: %s\n", fd, err_code, ec.message().c_str());return -1;}// 2.綁定地址端口// sockaddr_in: 以太網套接字地址數據結構,與結構sockaddr大小完全一致struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;// htons: 網絡字節序轉換函數,還包括htonl, ntohs, ntohl等,// 其中s是short數據類型的意思,l是long數據類型的意思,而h是host,即主機的意思,n是network,即網絡的意思,// htons: 表示對于short類型的變量,從主機字節序轉換為網絡字節序server_addr.sin_port = htons(server_port_);// inet_xxx: 字符串IP地址和二進制IP地址轉換函數// inet_pton: 將字符串類型的IP地址轉換為二進制類型,第1個參數表示網絡類型的協議族auto ret = inet_pton(AF_INET, server_ip_, &server_addr.sin_addr);if (ret != 1) {fprintf(stderr, "fail to inet_pton: %d\n", ret);return -1;}// sockaddr: 通用的套接字地址數據結構,它可以在不同協議族之間進行轉換,包含了地址、端口和IP地址的信息ret = bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));if (ret != 0) {auto err_code = get_error_code();std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to bind: %d, error code: %d, message: %s\n", ret, err_code, ec.message().c_str());return -1;}//std::this_thread::sleep_for(std::chrono::seconds(30)); // 為了驗證客戶端連接會超時// 3.監聽端口// listen: 用來初始化服務器可連接隊列,服務器處理客戶端連接請求的時候是順序處理的,同一時間僅能處理一個客戶端連接.// 當多個客戶端的連接請求同時到來的時候,服務器并不是同時處理,而是將不能處理的客戶端連接請求放到等待隊列中,這個隊列的長度有listen函數來定義// listen的第二個參數表示在accept函數處理之前在等待隊列中的客戶端的長度,如果超過這個長度,客戶端會返回一個錯誤ret = listen(fd, server_listen_queue_length_);if (ret != 0) {auto err_code = get_error_code();std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to listen: %d, error code: %d, message: %s\n", ret, err_code, ec.message().c_str());return -1;}while (1) {struct sockaddr_in client_addr;socklen_t length = sizeof(client_addr);// 4.接收客戶端的連接,在這個過程中客戶端與服務器進行三次握手,建立TCP連接// accept成功執行后,會返回一個新的套接字文件描述符來表示客戶端的連接,客戶端連接的信息可以通過這個新描述符來獲得// 當服務器成功處理客戶端的請求連接后,會有兩個文件描述符,老的文件描述符表示正在監聽的socket,新產生的文件描述符表示客戶端的連接auto fd2 = accept(fd, (struct sockaddr*)&client_addr, &length);if (fd2 < 0) {auto err_code = get_error_code();std::error_code ec(err_code, std::system_category());fprintf(stderr, "fail to accept: %d, error code: %d, message: %s\n", fd2, err_code, ec.message().c_str());continue;}struct in_addr addr;addr.s_addr = client_addr.sin_addr.s_addr;// inet_ntoa: 將二進制類型的IP地址轉換為字符串類型fprintf(stdout, "client ip: %s\n", inet_ntoa(addr));// 5.接收和發送數據// 連接上的每一個客戶都有單獨的線程來處理std::thread(calc_string_length, fd2).detach();}// 關閉套接字close(fd);return 0;
}
以上代碼段是服務器端程序實現,同時支持Windows和Linux上運行。
以下是Linux作為服務器端,Windows作為客戶端時的執行結果:服務器端程序先啟動
以上測試的完整代碼見:GitHub OpenSSL_Test/funset_socket.cpp
GitHub:https://github.com/fengbingchun/OpenSSL_Test
總結
以上是生活随笔為你收集整理的Windows/Linux TCP Socket网络编程简介及测试代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C和C++安全编码笔记:总结
- 下一篇: Ubuntu上Vim安装NERDTree