├── code ├── WSAAsyncSelect │ ├── WSAAsyncSelect.h │ ├── small.ico │ ├── WSAAsyncSelect.rc │ ├── WSAAsyncSelect.cpp │ ├── WSAAsyncSelect.ico │ ├── stdafx.cpp │ ├── targetver.h │ ├── stdafx.h │ ├── Resource.h │ ├── WSAAsyncSelect.sln │ ├── WSAAsyncSelect.vcxproj.filters │ └── WSAAsyncSelect.vcxproj ├── server.cpp ├── server2.cpp ├── poll_server.cpp ├── epoll_server.cpp ├── select_server.cpp ├── blocking_server.cpp ├── linux_nonblocking_connect.cpp ├── WSAEventSelect │ ├── WSAEventSelect.cpp │ ├── stdafx.cpp │ ├── targetver.h │ ├── stdafx.h │ ├── WSAEventSelect.sln │ ├── WSAEventSelect.vcxproj.filters │ └── WSAEventSelect.vcxproj ├── linux_nonblocking_connect_poll.cpp ├── gethostbyname_linux.cpp ├── blocking_client_recv.cpp ├── blocking_client_send.cpp ├── client.cpp ├── nagle_client.cpp ├── client2.cpp ├── server_recv_zero_bytes.cpp ├── nodelay_client.cpp ├── select_client_tvnull.cpp ├── nonblocking_client_recv.cpp ├── select_client_tv0.cpp ├── nonblocking_connect.cpp ├── nonblocking_client_send.cpp ├── nonblocking_client_send_zero_bytes.cpp └── select_client.cpp ├── 网络通信基础重难点解析 09 :阻塞与非阻塞的 socket 的各自适用场景.md ├── 网络通信基础重难点解析 10 :Linux EINTR 错误码.md ├── 网络通信基础重难点解析 15 :主机字节序和网络字节序.md ├── 网络通信基础重难点解析 05 :socket 的阻塞模式和非阻塞模式.md ├── README.md ├── 网络通信基础重难点解析 02:TCP 通信基本流程.md ├── 网络通信基础重难点解析 08 :connect 函数在阻塞和非阻塞模式下的行为.md ├── 网络通信基础重难点解析 14 :Windows 的 WSAAsyncSelect 网络通信模型.md ├── 网络通信基础重难点解析 01:常用 socket 函数基础.md ├── 网络通信基础重难点解析 07 :非阻塞模式下 send 和 recv 函数的返回值总结.md ├── 网络通信基础重难点解析 16 :域名解析 API 介绍.md ├── 网络通信基础重难点解析 11 :Linux poll 函数用法.md ├── 网络通信基础重难点解析 13 :Windows WSAEventSelect 网络通信模型.md ├── 网络通信基础重难点解析 03:bind 函数.md ├── 网络通信基础重难点解析 12 :Linux epoll 模型.md └── 网络通信基础重难点解析 04 :select 函数用法.md /code/WSAAsyncSelect/WSAAsyncSelect.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "resource.h" 4 | -------------------------------------------------------------------------------- /code/server.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/server.cpp -------------------------------------------------------------------------------- /code/server2.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/server2.cpp -------------------------------------------------------------------------------- /code/poll_server.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/poll_server.cpp -------------------------------------------------------------------------------- /code/epoll_server.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/epoll_server.cpp -------------------------------------------------------------------------------- /code/select_server.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/select_server.cpp -------------------------------------------------------------------------------- /code/blocking_server.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/blocking_server.cpp -------------------------------------------------------------------------------- /code/WSAAsyncSelect/small.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/WSAAsyncSelect/small.ico -------------------------------------------------------------------------------- /code/linux_nonblocking_connect.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/linux_nonblocking_connect.cpp -------------------------------------------------------------------------------- /code/WSAAsyncSelect/WSAAsyncSelect.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/WSAAsyncSelect/WSAAsyncSelect.rc -------------------------------------------------------------------------------- /code/WSAAsyncSelect/WSAAsyncSelect.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/WSAAsyncSelect/WSAAsyncSelect.cpp -------------------------------------------------------------------------------- /code/WSAAsyncSelect/WSAAsyncSelect.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/WSAAsyncSelect/WSAAsyncSelect.ico -------------------------------------------------------------------------------- /code/WSAEventSelect/WSAEventSelect.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/WSAEventSelect/WSAEventSelect.cpp -------------------------------------------------------------------------------- /code/linux_nonblocking_connect_poll.cpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lhy12315/easyserverdev/HEAD/code/linux_nonblocking_connect_poll.cpp -------------------------------------------------------------------------------- /code/WSAAsyncSelect/stdafx.cpp: -------------------------------------------------------------------------------- 1 | // stdafx.cpp : source file that includes just the standard includes 2 | // WSAAsyncSelect.pch will be the pre-compiled header 3 | // stdafx.obj will contain the pre-compiled type information 4 | 5 | #include "stdafx.h" 6 | 7 | // TODO: reference any additional headers you need in STDAFX.H 8 | // and not in this file 9 | -------------------------------------------------------------------------------- /code/WSAEventSelect/stdafx.cpp: -------------------------------------------------------------------------------- 1 | // stdafx.cpp : source file that includes just the standard includes 2 | // WSAEventSelect.pch will be the pre-compiled header 3 | // stdafx.obj will contain the pre-compiled type information 4 | 5 | #include "stdafx.h" 6 | 7 | // TODO: reference any additional headers you need in STDAFX.H 8 | // and not in this file 9 | -------------------------------------------------------------------------------- /code/WSAAsyncSelect/targetver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Including SDKDDKVer.h defines the highest available Windows platform. 4 | 5 | // If you wish to build your application for a previous Windows platform, include WinSDKVer.h and 6 | // set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. 7 | 8 | #include 9 | -------------------------------------------------------------------------------- /code/WSAEventSelect/targetver.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Including SDKDDKVer.h defines the highest available Windows platform. 4 | 5 | // If you wish to build your application for a previous Windows platform, include WinSDKVer.h and 6 | // set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. 7 | 8 | #include 9 | -------------------------------------------------------------------------------- /code/WSAEventSelect/stdafx.h: -------------------------------------------------------------------------------- 1 | // stdafx.h : include file for standard system include files, 2 | // or project specific include files that are used frequently, but 3 | // are changed infrequently 4 | // 5 | 6 | #pragma once 7 | 8 | #include "targetver.h" 9 | 10 | #include 11 | #include 12 | 13 | 14 | 15 | // TODO: reference additional headers your program requires here 16 | -------------------------------------------------------------------------------- /code/WSAAsyncSelect/stdafx.h: -------------------------------------------------------------------------------- 1 | // stdafx.h : include file for standard system include files, 2 | // or project specific include files that are used frequently, but 3 | // are changed infrequently 4 | // 5 | 6 | #pragma once 7 | 8 | #include "targetver.h" 9 | 10 | #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers 11 | // Windows Header Files: 12 | #include 13 | 14 | // C RunTime Header Files 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | 21 | // TODO: reference additional headers your program requires here 22 | -------------------------------------------------------------------------------- /code/WSAAsyncSelect/Resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by WSAAsyncSelect.rc 4 | // 5 | 6 | #define IDS_APP_TITLE 103 7 | 8 | #define IDR_MAINFRAME 128 9 | #define IDD_WSAASYNCSELECT_DIALOG 102 10 | #define IDD_ABOUTBOX 103 11 | #define IDM_ABOUT 104 12 | #define IDM_EXIT 105 13 | #define IDI_WSAASYNCSELECT 107 14 | #define IDI_SMALL 108 15 | #define IDC_WSAASYNCSELECT 109 16 | #define IDC_MYICON 2 17 | #ifndef IDC_STATIC 18 | #define IDC_STATIC -1 19 | #endif 20 | // Next default values for new objects 21 | // 22 | #ifdef APSTUDIO_INVOKED 23 | #ifndef APSTUDIO_READONLY_SYMBOLS 24 | 25 | #define _APS_NO_MFC 130 26 | #define _APS_NEXT_RESOURCE_VALUE 129 27 | #define _APS_NEXT_COMMAND_VALUE 32771 28 | #define _APS_NEXT_CONTROL_VALUE 1000 29 | #define _APS_NEXT_SYMED_VALUE 110 30 | #endif 31 | #endif 32 | -------------------------------------------------------------------------------- /网络通信基础重难点解析 09 :阻塞与非阻塞的 socket 的各自适用场景.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 09 :阻塞与非阻塞的 socket 的各自适用场景 2 | 3 | 4 | 5 | #### 阻塞与非阻塞的 socket 的各自适用场景 6 | 7 | 阻塞的 socket 函数在调用 **send**、**recv**、**connect**、**accept** 等函数时,如果特定的条件不满足,就会阻塞其调用线程直至超时,非阻塞的 socket 恰恰相反。这并不意味着非阻塞模式的 socket 模式比阻塞模式的 socket 模式好,二者各有优缺点。 8 | 9 | 非阻塞模式的 socket,一般用于需要支持高并发多 QPS 的场景下(如服务器程序),但是正如前文所述,这种模式让程序执行流和控制逻辑变复杂;相反,阻塞模式逻辑简单,程序结构简单明了,常用于一些特殊的场景。这里举两个应用的场景: 10 | 11 | **示例一** 12 | 13 | 程序需要临时发送一个文件,文件分段发送,每段对端都会的给与一个应答,程序可以单独开一个任务线程,在这个任务线程函数里面,使用先 send 后 recv 再 send 再 recv 的模式,每次 send 和 recv 都是阻塞式的。 14 | 15 | **示例二** 16 | 17 | A 端与 B 端之间只有问答模式,即 A 发送给 B 一个请求,B 必定会应答给 A 一个响应,除此以外,B 不会给 A 推送任何数据,也可以采取阻塞模式,每次 send 完请求后,就可以直接使用阻塞式的 recv 去接受一定要有的应答包。 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ------ 26 | 27 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 28 | 29 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 30 | 31 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /code/WSAAsyncSelect/WSAAsyncSelect.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.40629.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WSAAsyncSelect", "WSAAsyncSelect.vcxproj", "{87BE6464-6DE0-484B-8E81-57C8AB0C84CB}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Win32 = Debug|Win32 11 | Release|Win32 = Release|Win32 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {87BE6464-6DE0-484B-8E81-57C8AB0C84CB}.Debug|Win32.ActiveCfg = Debug|Win32 15 | {87BE6464-6DE0-484B-8E81-57C8AB0C84CB}.Debug|Win32.Build.0 = Debug|Win32 16 | {87BE6464-6DE0-484B-8E81-57C8AB0C84CB}.Release|Win32.ActiveCfg = Release|Win32 17 | {87BE6464-6DE0-484B-8E81-57C8AB0C84CB}.Release|Win32.Build.0 = Release|Win32 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /code/WSAEventSelect/WSAEventSelect.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.40629.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WSAEventSelect", "WSAEventSelect.vcxproj", "{7D04DFA2-70C2-4F25-AC66-F0F587648169}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Win32 = Debug|Win32 11 | Release|Win32 = Release|Win32 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {7D04DFA2-70C2-4F25-AC66-F0F587648169}.Debug|Win32.ActiveCfg = Debug|Win32 15 | {7D04DFA2-70C2-4F25-AC66-F0F587648169}.Debug|Win32.Build.0 = Debug|Win32 16 | {7D04DFA2-70C2-4F25-AC66-F0F587648169}.Release|Win32.ActiveCfg = Release|Win32 17 | {7D04DFA2-70C2-4F25-AC66-F0F587648169}.Release|Win32.Build.0 = Release|Win32 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /code/gethostbyname_linux.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | //extern int h_errno; 9 | 10 | bool connect_to_server(const char* server, short port) 11 | { 12 | int hSocket = socket(AF_INET, SOCK_STREAM, 0); 13 | if (hSocket == -1) 14 | return false; 15 | 16 | struct sockaddr_in addrSrv = { 0 }; 17 | struct hostent* pHostent = NULL; 18 | //unsigned int addr = 0; 19 | 20 | //如果传入的参数 server 的值是 somesite.com 这种域名域名形式则 if 条件成立, 21 | //接着调用 gethostbyname 解析域名为 4 字节的 ip 地址(整型) 22 | if (addrSrv.sin_addr.s_addr = inet_addr(server) == INADDR_NONE) 23 | { 24 | pHostent = gethostbyname(server); 25 | if (pHostent == NULL) 26 | return false; 27 | 28 | //当存在多个域名时,我们只取第一个 29 | addrSrv.sin_addr.s_addr = *((unsigned long*)pHostent->h_addr_list[0]); 30 | } 31 | 32 | addrSrv.sin_family = AF_INET; 33 | addrSrv.sin_port = htons(port); 34 | int ret = connect(hSocket, (struct sockaddr*)&addrSrv, sizeof(addrSrv)); 35 | if (ret == -1) 36 | return false; 37 | 38 | return true; 39 | } 40 | 41 | int main() 42 | { 43 | if (connect_to_server("baidu.com", 80)) 44 | printf("connect successfully.\n"); 45 | else 46 | printf("connect error.\n"); 47 | 48 | return 0; 49 | } -------------------------------------------------------------------------------- /code/blocking_client_recv.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证阻塞模式下recv函数的行为,client端,blocking_client_recv.cpp 3 | * zhangyl 2018.12.17 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define SERVER_ADDRESS "127.0.0.1" 13 | #define SERVER_PORT 3000 14 | #define SEND_DATA "helloworld" 15 | 16 | int main(int argc, char* argv[]) 17 | { 18 | //1.创建一个socket 19 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 20 | if (clientfd == -1) 21 | { 22 | std::cout << "create client socket error." << std::endl; 23 | return -1; 24 | } 25 | 26 | //2.连接服务器 27 | struct sockaddr_in serveraddr; 28 | serveraddr.sin_family = AF_INET; 29 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 30 | serveraddr.sin_port = htons(SERVER_PORT); 31 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 32 | { 33 | std::cout << "connect socket error." << std::endl; 34 | close(clientfd); 35 | return -1; 36 | } 37 | 38 | //直接调用recv函数,程序会阻塞在recv函数调用处 39 | char recvbuf[32] = {0}; 40 | int ret = recv(clientfd, recvbuf, 32, 0); 41 | if (ret > 0) 42 | { 43 | std::cout << "recv successfully." << std::endl; 44 | } 45 | else 46 | { 47 | std::cout << "recv data error." << std::endl; 48 | } 49 | 50 | //5. 关闭socket 51 | close(clientfd); 52 | 53 | return 0; 54 | } -------------------------------------------------------------------------------- /code/WSAEventSelect/WSAEventSelect.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Header Files 23 | 24 | 25 | Header Files 26 | 27 | 28 | 29 | 30 | Source Files 31 | 32 | 33 | Source Files 34 | 35 | 36 | -------------------------------------------------------------------------------- /code/blocking_client_send.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证阻塞模式下send函数的行为,client端 3 | * zhangyl 2018.12.17 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define SERVER_ADDRESS "127.0.0.1" 13 | #define SERVER_PORT 3000 14 | #define SEND_DATA "helloworld" 15 | 16 | int main(int argc, char* argv[]) 17 | { 18 | //1.创建一个socket 19 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 20 | if (clientfd == -1) 21 | { 22 | std::cout << "create client socket error." << std::endl; 23 | return -1; 24 | } 25 | 26 | //2.连接服务器 27 | struct sockaddr_in serveraddr; 28 | serveraddr.sin_family = AF_INET; 29 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 30 | serveraddr.sin_port = htons(SERVER_PORT); 31 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 32 | { 33 | std::cout << "connect socket error." << std::endl; 34 | close(clientfd); 35 | return -1; 36 | } 37 | 38 | //3. 不断向服务器发送数据,或者出错退出 39 | int count = 0; 40 | while (true) 41 | { 42 | int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); 43 | if (ret != strlen(SEND_DATA)) 44 | { 45 | std::cout << "send data error." << std::endl; 46 | break; 47 | } 48 | else 49 | { 50 | count ++; 51 | std::cout << "send data successfully, count = " << count << std::endl; 52 | } 53 | } 54 | 55 | //5. 关闭socket 56 | close(clientfd); 57 | 58 | return 0; 59 | } -------------------------------------------------------------------------------- /网络通信基础重难点解析 10 :Linux EINTR 错误码.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 10 :Linux EINTR 错误码 2 | 3 | ### Linux EINTR 错误码 4 | 5 | 在类 Unix 操作系统中(当然也包括 Linux 系统),当我们调用一些 socket 函数时(connect、send、recv、epoll_wait 等),除了函数调用出错会返回 **-1**,这些函数可能被信号中断也会返回 **-1**,此时我们可以通过错误码 **errno** 判断是不是 **EINTR ** 来确定是不是被信号中断。在实际编码的时候,请读者务必要考虑到这种情况。这也是上文中很多代码要专门判断错误码是不是 **EINTR**,如果是,说明被信号中断,我们需要再次调用这个函数进行重试。千万不要一看到返回值是 -1,草草认定这些调用失败,进而做出错误逻辑判断。 6 | 7 | ``` 8 | bool SendData(const char* buf , int buf_length) 9 | { 10 | //已发送的字节数目 11 | int sent_bytes = 0; 12 | int ret = 0; 13 | while (true) 14 | { 15 | ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0); 16 | if (nRet == -1) 17 | { 18 | if (errno == EWOULDBLOCK) 19 | { 20 | //严谨的做法,这里如果发不出去,应该缓存尚未发出去的数据,后面介绍 21 | break; 22 | } 23 | else if (errno == EINTR) 24 | continue; 25 | else 26 | return false; 27 | } 28 | else if (nRet == 0) 29 | { 30 | return false; 31 | } 32 | 33 | sent_bytes += ret; 34 | if (sent_bytes == buf_length) 35 | break; 36 | 37 | //稍稍降低 CPU 的使用率 38 | usleep(1); 39 | } 40 | 41 | return true; 42 | } 43 | ``` 44 | 45 | 46 | 47 | ------ 48 | 49 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 50 | 51 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 52 | 53 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /code/client.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * TCP客户端通信基本流程 3 | * zhangyl 2018.12.13 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define SERVER_ADDRESS "127.0.0.1" 13 | #define SERVER_PORT 3000 14 | #define SEND_DATA "helloworld" 15 | 16 | int main(int argc, char* argv[]) 17 | { 18 | //1.创建一个socket 19 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 20 | if (clientfd == -1) 21 | { 22 | std::cout << "create client socket error." << std::endl; 23 | return -1; 24 | } 25 | 26 | //2.连接服务器 27 | struct sockaddr_in serveraddr; 28 | serveraddr.sin_family = AF_INET; 29 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 30 | serveraddr.sin_port = htons(SERVER_PORT); 31 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 32 | { 33 | std::cout << "connect socket error." << std::endl; 34 | close(clientfd); 35 | return -1; 36 | } 37 | 38 | //3. 向服务器发送数据 39 | int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); 40 | if (ret != strlen(SEND_DATA)) 41 | { 42 | std::cout << "send data error." << std::endl; 43 | close(clientfd); 44 | return -1; 45 | } 46 | 47 | std::cout << "send data successfully, data: " << SEND_DATA << std::endl; 48 | 49 | //4. 从客户端收取数据 50 | char recvBuf[32] = {0}; 51 | ret = recv(clientfd, recvBuf, 32, 0); 52 | if (ret > 0) 53 | { 54 | std::cout << "recv data successfully, data: " << recvBuf << std::endl; 55 | } 56 | else 57 | { 58 | std::cout << "recv data error, data: " << recvBuf << std::endl; 59 | } 60 | 61 | //5. 关闭socket 62 | close(clientfd); 63 | 64 | return 0; 65 | } -------------------------------------------------------------------------------- /code/nagle_client.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证nagle算法 3 | * zhangyl 2018.12.13 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define SERVER_ADDRESS "127.0.0.1" 13 | #define SERVER_PORT 3000 14 | #define SEND_DATA "helloworld" 15 | 16 | int main(int argc, char* argv[]) 17 | { 18 | //1.创建一个socket 19 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 20 | if (clientfd == -1) 21 | { 22 | std::cout << "create client socket error." << std::endl; 23 | return -1; 24 | } 25 | 26 | //2.连接服务器 27 | struct sockaddr_in serveraddr; 28 | serveraddr.sin_family = AF_INET; 29 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 30 | serveraddr.sin_port = htons(SERVER_PORT); 31 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 32 | { 33 | std::cout << "connect socket error." << std::endl; 34 | return -1; 35 | } 36 | 37 | int count = 1000; 38 | int ret; 39 | //3. 连续向服务器发送1000次数据 40 | while (count > 0) 41 | { 42 | ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); 43 | if (ret != strlen(SEND_DATA)) 44 | { 45 | std::cout << "send data error." << std::endl; 46 | break; 47 | } 48 | 49 | count --; 50 | } 51 | 52 | if (count == 0) 53 | std::cout << "send data successfully" << std::endl; 54 | 55 | //4. 从客户端收取数据 56 | char recvBuf[32] = {0}; 57 | ret = recv(clientfd, recvBuf, 32, 0); 58 | if (ret > 0) 59 | { 60 | std::cout << "recv data successfully, data: " << recvBuf << std::endl; 61 | } 62 | else 63 | { 64 | std::cout << "recv data error, data: " << recvBuf << std::endl; 65 | } 66 | 67 | //5. 关闭socket 68 | close(clientfd); 69 | 70 | return 0; 71 | } -------------------------------------------------------------------------------- /code/client2.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * TCP客户端通信基本流程 3 | * zhangyl 2018.12.13 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define SERVER_ADDRESS "127.0.0.1" 13 | #define SERVER_PORT 3000 14 | #define SEND_DATA "helloworld" 15 | 16 | int main(int argc, char* argv[]) 17 | { 18 | //1.创建一个socket 19 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 20 | if (clientfd == -1) 21 | { 22 | std::cout << "create client socket error." << std::endl; 23 | return -1; 24 | } 25 | 26 | //2.连接服务器 27 | struct sockaddr_in serveraddr; 28 | serveraddr.sin_family = AF_INET; 29 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 30 | serveraddr.sin_port = htons(SERVER_PORT); 31 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 32 | { 33 | std::cout << "connect socket error." << std::endl; 34 | close(clientfd); 35 | return -1; 36 | } 37 | 38 | //3. 向服务器发送数据 39 | int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); 40 | if (ret != strlen(SEND_DATA)) 41 | { 42 | std::cout << "send data error." << std::endl; 43 | close(clientfd); 44 | return -1; 45 | } 46 | 47 | std::cout << "send data successfully, data: " << SEND_DATA << std::endl; 48 | 49 | //4. 从客户端收取数据 50 | char recvBuf[32] = {0}; 51 | ret = recv(clientfd, recvBuf, 32, 0); 52 | if (ret > 0) 53 | { 54 | std::cout << "recv data successfully, data: " << recvBuf << std::endl; 55 | } 56 | else 57 | { 58 | std::cout << "recv data error, data: " << recvBuf << std::endl; 59 | } 60 | 61 | //5. 关闭socket 62 | //close(clientfd); 63 | //这里仅仅是为了让客户端程序不退出 64 | while (true) 65 | { 66 | sleep(3); 67 | } 68 | 69 | return 0; 70 | } -------------------------------------------------------------------------------- /code/WSAAsyncSelect/WSAAsyncSelect.vcxproj.filters: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Header Files 23 | 24 | 25 | Header Files 26 | 27 | 28 | Header Files 29 | 30 | 31 | Header Files 32 | 33 | 34 | 35 | 36 | Source Files 37 | 38 | 39 | Source Files 40 | 41 | 42 | 43 | 44 | Resource Files 45 | 46 | 47 | 48 | 49 | Resource Files 50 | 51 | 52 | Resource Files 53 | 54 | 55 | -------------------------------------------------------------------------------- /code/server_recv_zero_bytes.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证recv函数接受0字节的行为,server端,server_recv_zero_bytes.cpp 3 | * zhangyl 2018.12.17 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | int main(int argc, char* argv[]) 14 | { 15 | //1.创建一个侦听socket 16 | int listenfd = socket(AF_INET, SOCK_STREAM, 0); 17 | if (listenfd == -1) 18 | { 19 | std::cout << "create listen socket error." << std::endl; 20 | return -1; 21 | } 22 | 23 | //2.初始化服务器地址 24 | struct sockaddr_in bindaddr; 25 | bindaddr.sin_family = AF_INET; 26 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 27 | bindaddr.sin_port = htons(3000); 28 | if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 29 | { 30 | std::cout << "bind listen socket error." << std::endl; 31 | close(listenfd); 32 | return -1; 33 | } 34 | 35 | //3.启动侦听 36 | if (listen(listenfd, SOMAXCONN) == -1) 37 | { 38 | std::cout << "listen error." << std::endl; 39 | close(listenfd); 40 | return -1; 41 | } 42 | 43 | int clientfd; 44 | 45 | struct sockaddr_in clientaddr; 46 | socklen_t clientaddrlen = sizeof(clientaddr); 47 | //4. 接受客户端连接 48 | clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); 49 | if (clientfd != -1) 50 | { 51 | while (true) 52 | { 53 | char recvBuf[32] = {0}; 54 | //5. 从客户端接受数据,客户端没有数据来的时候会recv函数会阻塞 55 | int ret = recv(clientfd, recvBuf, 32, 0); 56 | if (ret > 0) 57 | { 58 | std::cout << "recv data from client, data: " << recvBuf << std::endl; 59 | } 60 | else if (ret == 0) 61 | { 62 | std::cout << "recv 0 byte data." << std::endl; 63 | continue; 64 | } 65 | else 66 | { 67 | //出错 68 | std::cout << "recv data error." << std::endl; 69 | break; 70 | } 71 | } 72 | } 73 | 74 | 75 | //关闭客户端socket 76 | close(clientfd); 77 | //7.关闭侦听socket 78 | close(listenfd); 79 | 80 | return 0; 81 | } -------------------------------------------------------------------------------- /code/nodelay_client.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证禁用nagle算法 3 | * zhangyl 2018.12.13 4 | */ 5 | #include 6 | #include 7 | #include 8 | //for TCP_NODELAY definition 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define SERVER_ADDRESS "127.0.0.1" 15 | #define SERVER_PORT 3000 16 | #define SEND_DATA "helloworld" 17 | 18 | int main(int argc, char* argv[]) 19 | { 20 | //1.创建一个socket 21 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 22 | if (clientfd == -1) 23 | { 24 | std::cout << "create client socket error." << std::endl; 25 | return -1; 26 | } 27 | 28 | //2.连接服务器 29 | struct sockaddr_in serveraddr; 30 | serveraddr.sin_family = AF_INET; 31 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 32 | serveraddr.sin_port = htons(SERVER_PORT); 33 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 34 | { 35 | std::cout << "connect socket error." << std::endl; 36 | return -1; 37 | } 38 | 39 | //禁用nagle算法 40 | int optval = 1; 41 | if (setsockopt(clientfd, IPPROTO_TCP, TCP_NODELAY, &optval, static_cast(sizeof optval)) != 0) 42 | { 43 | std::cout << "set nodelay error." << std::endl; 44 | close(clientfd); 45 | return -1; 46 | } 47 | 48 | int count = 1000; 49 | int ret; 50 | //3. 连续向服务器发送1000次数据 51 | while (count > 0) 52 | { 53 | ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); 54 | if (ret != strlen(SEND_DATA)) 55 | { 56 | std::cout << "send data error." << std::endl; 57 | break; 58 | } 59 | 60 | count --; 61 | } 62 | 63 | if (count == 0) 64 | std::cout << "send data successfully" << std::endl; 65 | 66 | //4. 从客户端收取数据 67 | char recvBuf[32] = {0}; 68 | ret = recv(clientfd, recvBuf, 32, 0); 69 | if (ret > 0) 70 | { 71 | std::cout << "recv data successfully, data: " << recvBuf << std::endl; 72 | } 73 | else 74 | { 75 | std::cout << "recv data error, data: " << recvBuf << std::endl; 76 | } 77 | 78 | //5. 关闭socket 79 | close(clientfd); 80 | 81 | return 0; 82 | } -------------------------------------------------------------------------------- /code/select_client_tvnull.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证select时间参数设置为NULL,select_client_tvnull.cpp 3 | * zhangyl 2018.12.25 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define SERVER_ADDRESS "127.0.0.1" 15 | #define SERVER_PORT 3000 16 | 17 | int main(int argc, char* argv[]) 18 | { 19 | //创建一个socket 20 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 21 | if (clientfd == -1) 22 | { 23 | std::cout << "create client socket error." << std::endl; 24 | return -1; 25 | } 26 | 27 | //连接服务器 28 | struct sockaddr_in serveraddr; 29 | serveraddr.sin_family = AF_INET; 30 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 31 | serveraddr.sin_port = htons(SERVER_PORT); 32 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 33 | { 34 | std::cout << "connect socket error." << std::endl; 35 | close(clientfd); 36 | return -1; 37 | } 38 | 39 | int ret; 40 | while (true) 41 | { 42 | fd_set readset; 43 | FD_ZERO(&readset); 44 | //将侦听socket加入到待检测的可读事件中去 45 | FD_SET(clientfd, &readset); 46 | //timeval tm; 47 | //tm.tv_sec = 0; 48 | //tm.tv_usec = 0; 49 | 50 | //暂且只检测可读事件,不检测可写和异常事件 51 | ret = select(clientfd + 1, &readset, NULL, NULL, NULL); 52 | if (ret == -1) 53 | { 54 | //除了被信号中断的情形,其他情况都是出错 55 | if (errno != EINTR) 56 | break; 57 | } else if (ret == 0){ 58 | //select函数超时 59 | std::cout << "no event in specific time interval." << std::endl; 60 | continue; 61 | } else { 62 | if (FD_ISSET(clientfd, &readset)) 63 | { 64 | //检测到可读事件 65 | char recvbuf[32]; 66 | memset(recvbuf, 0, sizeof(recvbuf)); 67 | //假设对端发数据的时候不超过31个字符。 68 | int n = recv(clientfd, recvbuf, 32, 0); 69 | if (n < 0) 70 | { 71 | //除了被信号中断的情形,其他情况都是出错 72 | if (errno != EINTR) 73 | break; 74 | } else if (n == 0) { 75 | //对端关闭了连接 76 | break; 77 | } else { 78 | std::cout << "recv data: " << recvbuf << std::endl; 79 | } 80 | } 81 | else 82 | { 83 | std::cout << "other socket event." << std::endl; 84 | } 85 | } 86 | } 87 | 88 | 89 | //关闭socket 90 | close(clientfd); 91 | 92 | return 0; 93 | } -------------------------------------------------------------------------------- /code/nonblocking_client_recv.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证阻塞模式下recv函数的行为,client端,blocking_client_recv.cpp 3 | * zhangyl 2018.12.17 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define SERVER_ADDRESS "127.0.0.1" 17 | #define SERVER_PORT 3000 18 | #define SEND_DATA "helloworld" 19 | 20 | int main(int argc, char* argv[]) 21 | { 22 | //1.创建一个socket 23 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 24 | if (clientfd == -1) 25 | { 26 | std::cout << "create client socket error." << std::endl; 27 | return -1; 28 | } 29 | 30 | //2.连接服务器 31 | struct sockaddr_in serveraddr; 32 | serveraddr.sin_family = AF_INET; 33 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 34 | serveraddr.sin_port = htons(SERVER_PORT); 35 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 36 | { 37 | std::cout << "connect socket error." << std::endl; 38 | close(clientfd); 39 | return -1; 40 | } 41 | 42 | //连接成功以后,我们再将 clientfd 设置成非阻塞模式, 43 | //不能在创建时就设置,这样会影响到 connect 函数的行为 44 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 45 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 46 | if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 47 | { 48 | close(clientfd); 49 | std::cout << "set socket to nonblock error." << std::endl; 50 | return -1; 51 | } 52 | 53 | //直接调用recv函数,程序会阻塞在recv函数调用处 54 | while (true) 55 | { 56 | char recvbuf[32] = {0}; 57 | int ret = recv(clientfd, recvbuf, 32, 0); 58 | if (ret > 0) 59 | { 60 | //收到了数据 61 | std::cout << "recv successfully." << std::endl; 62 | } 63 | else if (ret == 0) 64 | { 65 | //对端关闭了连接 66 | std::cout << "peer close the socket." << std::endl; 67 | break; 68 | } 69 | else if (ret == -1) 70 | { 71 | if (errno == EWOULDBLOCK) 72 | { 73 | std::cout << "There is no data available now." << std::endl; 74 | } 75 | else if (errno == EINTR) 76 | { 77 | //如果被信号中断了,则继续重试recv函数 78 | std::cout << "recv data interrupted by signal." << std::endl; 79 | } else 80 | { 81 | //真的出错了 82 | break; 83 | } 84 | } 85 | } 86 | 87 | //5. 关闭socket 88 | close(clientfd); 89 | 90 | return 0; 91 | } -------------------------------------------------------------------------------- /code/select_client_tv0.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证select时间参数设置为0,select_client_tv0.cpp 3 | * zhangyl 2018.12.25 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define SERVER_ADDRESS "127.0.0.1" 15 | #define SERVER_PORT 3000 16 | 17 | int main(int argc, char* argv[]) 18 | { 19 | //创建一个socket 20 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 21 | if (clientfd == -1) 22 | { 23 | std::cout << "create client socket error." << std::endl; 24 | return -1; 25 | } 26 | 27 | //连接服务器 28 | struct sockaddr_in serveraddr; 29 | serveraddr.sin_family = AF_INET; 30 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 31 | serveraddr.sin_port = htons(SERVER_PORT); 32 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 33 | { 34 | std::cout << "connect socket error." << std::endl; 35 | close(clientfd); 36 | return -1; 37 | } 38 | 39 | int ret; 40 | while (true) 41 | { 42 | fd_set readset; 43 | FD_ZERO(&readset); 44 | //将侦听socket加入到待检测的可读事件中去 45 | FD_SET(clientfd, &readset); 46 | timeval tm; 47 | tm.tv_sec = 0; 48 | tm.tv_usec = 0; 49 | 50 | //暂且只检测可读事件,不检测可写和异常事件 51 | ret = select(clientfd + 1, &readset, NULL, NULL, &tm); 52 | std::cout << "tm.tv_sec: " << tm.tv_sec << ", tm.tv_usec: " << tm.tv_usec << std::endl; 53 | if (ret == -1) 54 | { 55 | //除了被信号中断的情形,其他情况都是出错 56 | if (errno != EINTR) 57 | break; 58 | } else if (ret == 0){ 59 | //select函数超时 60 | std::cout << "no event in specific time interval." << std::endl; 61 | continue; 62 | } else { 63 | if (FD_ISSET(clientfd, &readset)) 64 | { 65 | //检测到可读事件 66 | char recvbuf[32]; 67 | memset(recvbuf, 0, sizeof(recvbuf)); 68 | //假设对端发数据的时候不超过31个字符。 69 | int n = recv(clientfd, recvbuf, 32, 0); 70 | if (n < 0) 71 | { 72 | //除了被信号中断的情形,其他情况都是出错 73 | if (errno != EINTR) 74 | break; 75 | } else if (n == 0) { 76 | //对端关闭了连接 77 | break; 78 | } else { 79 | std::cout << "recv data: " << recvbuf << std::endl; 80 | } 81 | } 82 | else 83 | { 84 | std::cout << "other socket event." << std::endl; 85 | } 86 | } 87 | } 88 | 89 | 90 | //关闭socket 91 | close(clientfd); 92 | 93 | return 0; 94 | } -------------------------------------------------------------------------------- /code/nonblocking_connect.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 异步的connect写法,nonblocking_connect.cpp 3 | * zhangyl 2018.12.17 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define SERVER_ADDRESS "127.0.0.1" 16 | #define SERVER_PORT 3000 17 | #define SEND_DATA "helloworld" 18 | 19 | int main(int argc, char* argv[]) 20 | { 21 | //1.创建一个socket 22 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 23 | if (clientfd == -1) 24 | { 25 | std::cout << "create client socket error." << std::endl; 26 | return -1; 27 | } 28 | 29 | //连接成功以后,我们再将 clientfd 设置成非阻塞模式, 30 | //不能在创建时就设置,这样会影响到 connect 函数的行为 31 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 32 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 33 | if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 34 | { 35 | close(clientfd); 36 | std::cout << "set socket to nonblock error." << std::endl; 37 | return -1; 38 | } 39 | 40 | //2.连接服务器 41 | struct sockaddr_in serveraddr; 42 | serveraddr.sin_family = AF_INET; 43 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 44 | serveraddr.sin_port = htons(SERVER_PORT); 45 | for (;;) 46 | { 47 | int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); 48 | if (ret == 0) 49 | { 50 | std::cout << "connect to server successfully." << std::endl; 51 | close(clientfd); 52 | return 0; 53 | } 54 | else if (ret == -1) 55 | { 56 | if (errno == EINTR) 57 | { 58 | //connect 动作被信号中断,重试connect 59 | std::cout << "connecting interruptted by signal, try again." << std::endl; 60 | continue; 61 | } else if (errno == EINPROGRESS) 62 | { 63 | //连接正在尝试中 64 | break; 65 | } else { 66 | //真的出错了, 67 | close(clientfd); 68 | return -1; 69 | } 70 | } 71 | } 72 | 73 | fd_set writeset; 74 | FD_ZERO(&writeset); 75 | FD_SET(clientfd, &writeset); 76 | //可以利用tv_sec和tv_usec做更小精度的超时控制 77 | struct timeval tv; 78 | tv.tv_sec = 3; 79 | tv.tv_usec = 0; 80 | if (select(clientfd + 1, NULL, &writeset, NULL, &tv) == 1) 81 | { 82 | std::cout << "[select] connect to server successfully." << std::endl; 83 | } else { 84 | std::cout << "[select] connect to server error." << std::endl; 85 | } 86 | 87 | //5. 关闭socket 88 | close(clientfd); 89 | 90 | return 0; 91 | } -------------------------------------------------------------------------------- /code/nonblocking_client_send.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证非阻塞模式下send函数的行为,client端,nonblocking_client.cpp 3 | * zhangyl 2018.12.17 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define SERVER_ADDRESS "127.0.0.1" 16 | #define SERVER_PORT 3000 17 | #define SEND_DATA "helloworld" 18 | 19 | int main(int argc, char* argv[]) 20 | { 21 | //1.创建一个socket 22 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 23 | if (clientfd == -1) 24 | { 25 | std::cout << "create client socket error." << std::endl; 26 | return -1; 27 | } 28 | 29 | //2.连接服务器 30 | struct sockaddr_in serveraddr; 31 | serveraddr.sin_family = AF_INET; 32 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 33 | serveraddr.sin_port = htons(SERVER_PORT); 34 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 35 | { 36 | std::cout << "connect socket error." << std::endl; 37 | close(clientfd); 38 | return -1; 39 | } 40 | 41 | //连接成功以后,我们再将 clientfd 设置成非阻塞模式, 42 | //不能在创建时就设置,这样会影响到 connect 函数的行为 43 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 44 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 45 | if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 46 | { 47 | close(clientfd); 48 | std::cout << "set socket to nonblock error." << std::endl; 49 | return -1; 50 | } 51 | 52 | //3. 不断向服务器发送数据,或者出错退出 53 | int count = 0; 54 | while (true) 55 | { 56 | int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); 57 | if (ret == -1) 58 | { 59 | //非阻塞模式下send函数由于TCP窗口太小发不出去数据,错误码是EWOULDBLOCK 60 | if (errno == EWOULDBLOCK) 61 | { 62 | std::cout << "send data error as TCP Window size is too small." << std::endl; 63 | continue; 64 | } 65 | else if (errno == EINTR) 66 | { 67 | //如果被信号中断,我们继续重试 68 | std::cout << "sending data interrupted by signal." << std::endl; 69 | continue; 70 | } 71 | else 72 | { 73 | std::cout << "send data error." << std::endl; 74 | break; 75 | } 76 | } 77 | if (ret == 0) 78 | { 79 | //对端关闭了连接,我们也关闭 80 | std::cout << "send data error." << std::endl; 81 | close(clientfd); 82 | break; 83 | } 84 | else 85 | { 86 | count ++; 87 | std::cout << "send data successfully, count = " << count << std::endl; 88 | } 89 | } 90 | 91 | //5. 关闭socket 92 | close(clientfd); 93 | 94 | return 0; 95 | } -------------------------------------------------------------------------------- /code/nonblocking_client_send_zero_bytes.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证非阻塞模式下send函数发送0字节的行为,client端,nonblocking_client_send_zero_bytes.cpp 3 | * zhangyl 2018.12.17 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #define SERVER_ADDRESS "127.0.0.1" 16 | #define SERVER_PORT 3000 17 | #define SEND_DATA "" 18 | 19 | int main(int argc, char* argv[]) 20 | { 21 | //1.创建一个socket 22 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 23 | if (clientfd == -1) 24 | { 25 | std::cout << "create client socket error." << std::endl; 26 | return -1; 27 | } 28 | 29 | //2.连接服务器 30 | struct sockaddr_in serveraddr; 31 | serveraddr.sin_family = AF_INET; 32 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 33 | serveraddr.sin_port = htons(SERVER_PORT); 34 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 35 | { 36 | std::cout << "connect socket error." << std::endl; 37 | close(clientfd); 38 | return -1; 39 | } 40 | 41 | //连接成功以后,我们再将 clientfd 设置成非阻塞模式, 42 | //不能在创建时就设置,这样会影响到 connect 函数的行为 43 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 44 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 45 | if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 46 | { 47 | close(clientfd); 48 | std::cout << "set socket to nonblock error." << std::endl; 49 | return -1; 50 | } 51 | 52 | //3. 不断向服务器发送数据,或者出错退出 53 | int count = 0; 54 | while (true) 55 | { 56 | //发送 0 字节的数据 57 | int ret = send(clientfd, SEND_DATA, 0, 0); 58 | if (ret == -1) 59 | { 60 | //非阻塞模式下send函数由于TCP窗口太小发不出去数据,错误码是EWOULDBLOCK 61 | if (errno == EWOULDBLOCK) 62 | { 63 | std::cout << "send data error as TCP Window size is too small." << std::endl; 64 | continue; 65 | } 66 | else if (errno == EINTR) 67 | { 68 | //如果被信号中断,我们继续重试 69 | std::cout << "sending data interrupted by signal." << std::endl; 70 | continue; 71 | } 72 | else 73 | { 74 | std::cout << "send data error." << std::endl; 75 | break; 76 | } 77 | } 78 | else if (ret == 0) 79 | { 80 | //对端关闭了连接,我们也关闭 81 | std::cout << "send 0 byte data." << std::endl; 82 | } 83 | else 84 | { 85 | count ++; 86 | std::cout << "send data successfully, count = " << count << std::endl; 87 | } 88 | 89 | //每三秒发一次 90 | sleep(3); 91 | } 92 | 93 | //5. 关闭socket 94 | close(clientfd); 95 | 96 | return 0; 97 | } -------------------------------------------------------------------------------- /code/select_client.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * 验证调用select后必须重设fd_set,select_client.cpp 3 | * zhangyl 2018.12.24 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define SERVER_ADDRESS "127.0.0.1" 15 | #define SERVER_PORT 3000 16 | 17 | int main(int argc, char* argv[]) 18 | { 19 | //创建一个socket 20 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 21 | if (clientfd == -1) 22 | { 23 | std::cout << "create client socket error." << std::endl; 24 | return -1; 25 | } 26 | 27 | //连接服务器 28 | struct sockaddr_in serveraddr; 29 | serveraddr.sin_family = AF_INET; 30 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 31 | serveraddr.sin_port = htons(SERVER_PORT); 32 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 33 | { 34 | std::cout << "connect socket error." << std::endl; 35 | close(clientfd); 36 | return -1; 37 | } 38 | 39 | fd_set readset; 40 | FD_ZERO(&readset); 41 | 42 | //将侦听socket加入到待检测的可读事件中去 43 | FD_SET(clientfd, &readset); 44 | timeval tm; 45 | tm.tv_sec = 5; 46 | tm.tv_usec = 0; 47 | int ret; 48 | int count = 0; 49 | fd_set backup_readset; 50 | memcpy(&backup_readset, &readset, sizeof(fd_set)); 51 | while (true) 52 | { 53 | if (memcmp(&readset, &backup_readset, sizeof(fd_set)) == 0) 54 | { 55 | std::cout << "equal" << std::endl; 56 | } 57 | else 58 | { 59 | std::cout << "not equal" << std::endl; 60 | } 61 | 62 | //暂且只检测可读事件,不检测可写和异常事件 63 | ret = select(clientfd + 1, &readset, NULL, NULL, &tm); 64 | std::cout << "tm.tv_sec: " << tm.tv_sec << ", tm.tv_usec: " << tm.tv_usec << std::endl; 65 | if (ret == -1) 66 | { 67 | //除了被信号中断的情形,其他情况都是出错 68 | if (errno != EINTR) 69 | break; 70 | } else if (ret == 0){ 71 | //select函数超时 72 | std::cout << "no event in specific time interval, count:" << count << std::endl; 73 | ++count; 74 | continue; 75 | } else { 76 | if (FD_ISSET(clientfd, &readset)) 77 | { 78 | //检测到可读事件 79 | char recvbuf[32]; 80 | memset(recvbuf, 0, sizeof(recvbuf)); 81 | //假设对端发数据的时候不超过31个字符。 82 | int n = recv(clientfd, recvbuf, 32, 0); 83 | if (n < 0) 84 | { 85 | //除了被信号中断的情形,其他情况都是出错 86 | if (errno != EINTR) 87 | break; 88 | } else if (n == 0) { 89 | //对端关闭了连接 90 | break; 91 | } else { 92 | std::cout << "recv data: " << recvbuf << std::endl; 93 | } 94 | } 95 | else 96 | { 97 | std::cout << "other socket event." << std::endl; 98 | } 99 | } 100 | } 101 | 102 | 103 | //关闭socket 104 | close(clientfd); 105 | 106 | return 0; 107 | } -------------------------------------------------------------------------------- /网络通信基础重难点解析 15 :主机字节序和网络字节序.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 15 :主机字节序和网络字节序 2 | 3 | 4 | 5 | ### 主机字节序和网络字节序 6 | 7 | #### 主机字节序 8 | 9 | 网络通信本质上是不同的机器进行数据交换,一般不同的机器有着不同的 CPU 型号,不同的 CPU 其**字节**序可能不一样。所谓**字节序**指的是对于存储需要多个字节(大于 1 字节)的整数来说,其每个字节在不同的机器内存中存储的顺序。这就是所谓的**主机字节序**,一般分为两类: 10 | 11 | - **little-endian** (LE,俗称**小端编码**或**小头编码**) 12 | 13 | 对于一个整数值,如果使用小端字节序,整数的**高**位存储在内存地址**高**的位置,整数的**低**位存储在内存地址**低**的位置上(所谓的**高高低低**),这种序列比较符合人的思维习惯。Intel x86 系列的系统使用的是小端编码方式。 14 | 15 | - **big-endian**(BE,俗称**大端编码**或**大头编码**) 16 | 17 | 对于一个整数值,如果使用大端字节序,整数的**高**位存储在内存地址**低**的位置,整数的**低**位存储在内存地址**高**的位置上(所谓的**高低低高**),这是最直观的字节序。Java 程序、Mac 机器上的程序一般是大端编码方式。 18 | 19 | 举个例子,对于内存中双字值 **0x10203040** (4 字节)的存储方式,如果使用**小端编码**,其内存中存储方式如下: 20 | 21 | ![](http://www.hootina.org/github_easyserverdev/20190319144307.png) 22 | 23 | 如果使用**大端编码** 来存储 **0x10203040**,在内存中存储示意图如下: 24 | 25 | ![](http://www.hootina.org/github_easyserverdev/20190319144516.png) 26 | 27 | >关于**大端**和**小端**一词来源于《格列佛游记》中的小人国故事,小人国中的两个国家因为吃鸡蛋应该先从鸡蛋的大端还是小端先磕破这一“宗教信仰”问题水火难容,频繁发生战争。 28 | 29 | 30 | 31 | #### 网络字节序 32 | 33 | **网络字节序**是 TCP/IP 协议中规定好的一种数据表示格式,它与具体的 CPU 类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用 **big-endian** 排序方式。因此为了不同的机器和系统可以正常交换数据,一般建议将需要传输的整型值转换成网络字节序,我们前面代码中使用端口时即将端口号数值从本地字节序转换成网络字节序: 34 | 35 | ``` 36 | //2.初始化服务器地址 37 | struct sockaddr_in bindaddr; 38 | bindaddr.sin_family = AF_INET; 39 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 40 | //将端口号3000转换成网络字节序 41 | bindaddr.sin_port = htons(3000); 42 | if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 43 | { 44 | std::cout << "bind listen socket error." << std::endl; 45 | return -1; 46 | } 47 | ``` 48 | 49 | 50 | 51 | **htons** 函数即将一个 short 类型从本机字节序转换成网络字节序,你可以这么记忆这个函数:**h**ost **to** **n**et **s**hort => htons,与这个函数类似的还有一系列将整型转网络字节序的函数(以 Linux 系统为例): 52 | 53 | ``` 54 | #include 55 | 56 | //host to net long 57 | uint32_t htonl(uint32_t hostlong); 58 | //host to net short 59 | uint16_t htons(uint16_t hostshort); 60 | ``` 61 | 62 | 与此相反,当从网络上收到数据以后,如果需要将整数从网络字节序转换成本地字节序,也有对应的系列函数: 63 | 64 | ``` 65 | #include 66 | 67 | //net to host long 68 | uint32_t ntohl(uint32_t netlong); 69 | //net to host short 70 | uint16_t ntohs(uint16_t netshort); 71 | ``` 72 | 73 | 74 | 75 | 这类转换函数的实现原理也很简单,以本地字节序转网络字节序为例,如果发现本机字节序就是网络字节序(即本机字节序就是大端编码)则什么也不做,反之将字节顺序互换。 76 | 77 | 那如何判断本机字节序是不是网络字节序呢?可以随意找一个 2 字节的十六进制数值测试一下,例如 0x1234,如果本机字节序是小头编码,这值 12 存储在高地址字节中,34 存储在低地址字节中,这样当强行把 0x1234 转换成 1 字节的 char 时,高字节被丢弃,剩下低字节值,就是 34;反之,如果本机字节序是大端编码,则高地址字节中存储的是 34,低地址字节中存储的是 12,当强转成一个字节的 char 时,其值是 12,代码实现如下: 78 | 79 | ``` 80 | //判断本机是否是网络字节序 81 | bool isNetByteOrder() 82 | { 83 | unsigned short mode = 0x1234; 84 | char* pmode = (char*)&mode; 85 | //低字节放低位 小端字节 86 | if (*pmode == 0x34) 87 | return false; 88 | 89 | return true; 90 | } 91 | ``` 92 | 93 | 在上面的基础上,我们来实现一下 **htons** 函数: 94 | 95 | ``` 96 | uint16_t htons(uint16_t hostshort); 97 | { 98 | //如果已经本机字节序是网络字节序,则直接返回 99 | if (isNetByteOrder()) 100 | return hostshort; 101 | 102 | return ((uint16_t)(hostshort >> 8)) | ((uint16_t)((hostshort & 0x00ff) << 8)); 103 | } 104 | ``` 105 | 106 | 其他的函数实现原理类似,读者可以自己实现一下,这里不再重复介绍。 107 | 108 | 109 | 110 | ------ 111 | 112 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 113 | 114 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 115 | 116 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /网络通信基础重难点解析 05 :socket 的阻塞模式和非阻塞模式.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 05 :socket 的阻塞模式和非阻塞模式 2 | 3 | 4 | 5 | ### 4.5 socket 的阻塞模式和非阻塞模式 6 | 7 | 对 socket 在阻塞和非阻塞模式下的各个函数的行为差别深入的理解是掌握网络编程的基本要求之一,是重点也是难点。 8 | 9 | 阻塞和非阻塞模式下,我们常讨论的具有不同行为表现的 socket 函数一般有如下几个,见下表: 10 | 11 | - connect 12 | - accept 13 | - send (Linux 平台上对 socket 进行操作时也包括 **write** 函数,下文中对 send 函数的讨论也适用于 **write** 函数) 14 | - recv (Linux 平台上对 socket 进行操作时也包括 **read** 函数,下文中对 recv 函数的讨论也适用于 **read** 函数) 15 | 16 | 17 | 18 | 在正式讨论以上四个函数之前,我们先解释一下阻塞模式和非阻塞模式的概念。所谓**阻塞模式**,**就当某个函数“执行成功的条件”当前不能满足时,该函数会阻塞当前执行线程,程序执行流在超时时间到达或“执行成功的条件”满足后恢复继续执行**。而**非阻塞模式**恰恰相反,即使某个函数的“执行成功的条件”不当前不能满足,该函数也不会阻塞当前执行线程,而是立即返回,继续运行执行程序流。如果读者不太明白这两个定义也没关系,后面我们会以具体的示例来讲解这两种模式的区别。 19 | 20 | #### 如何将 socket 设置成非阻塞模式 21 | 22 | 无论是 Windows 还是 Linux 平台,默认创建的 socket 都是阻塞模式的。 23 | 24 | 在 Linux 平台上,我们可以使用 **fcntl() 函数**或 **ioctl() 函数**给创建的 socket 增加 **O_NONBLOCK** 标志来将 socket 设置成非阻塞模式。示例代码如下: 25 | 26 | ``` 27 | int oldSocketFlag = fcntl(sockfd, F_GETFL, 0); 28 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 29 | fcntl(sockfd, F_SETFL, newSocketFlag); 30 | ``` 31 | 32 | **ioctl() 函数** 与 **fcntl()** 函数使用方式基本一致,这里就不再给出示例代码了。 33 | 34 | 当然,Linux 下的 **socket()** 创建函数也可以直接在创建时将 socket 设置为非阻塞模式,**socket()** 函数的签名如下: 35 | 36 | ``` 37 | int socket(int domain, int type, int protocol); 38 | ``` 39 | 40 | 给 **type** 参数增加一个 **SOCK_NONBLOCK** 标志即可,例如: 41 | 42 | ``` 43 | int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP); 44 | ``` 45 | 46 | 不仅如此,Linux 系统下利用 accept() 函数返回的代表与客户端通信的 socket 也提供了一个扩展函数 **accept4()**,直接将 accept 函数返回的 socket 设置成非阻塞的。 47 | 48 | ``` 49 | int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 50 | int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags); 51 | ``` 52 | 53 | 只要将 **accept4()** 函数最后一个参数 **flags** 设置成 **SOCK_NONBLOCK** 即可。也就是说以下代码是等价的: 54 | 55 | ``` 56 | socklen_t addrlen = sizeof(clientaddr); 57 | int clientfd = accept4(listenfd, &clientaddr, &addrlen, SOCK_NONBLOCK); 58 | ``` 59 | 60 | ``` 61 | socklen_t addrlen = sizeof(clientaddr); 62 | int clientfd = accept(listenfd, &clientaddr, &addrlen); 63 | if (clientfd != -1) 64 | { 65 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 66 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 67 | fcntl(clientfd, F_SETFL, newSocketFlag); 68 | } 69 | ``` 70 | 71 | 72 | 73 | 在 Windows 平台上,可以调用 **ioctlsocket() 函数** 将 socket 设置成非阻塞模式,**ioctlsocket()** 签名如下: 74 | 75 | ``` 76 | int ioctlsocket(SOCKET s, long cmd, u_long *argp); 77 | ``` 78 | 79 | 将 **cmd** 参数设置为 **FIONBIO**,***argp** 设置为 **0** 即可将 socket 设置成阻塞模式,而将 ***argp** 设置成非 **0** 即可设置成非阻塞模式。示例如下: 80 | 81 | ``` 82 | //将 socket 设置成非阻塞模式 83 | u_long argp = 1; 84 | ioctlsocket(s, FIONBIO, &argp); 85 | 86 | //将 socket 设置成阻塞模式 87 | u_long argp = 0; 88 | ioctlsocket(s, FIONBIO, &argp); 89 | ``` 90 | 91 | Windows 平台需要注意一个地方,如果对一个 socket 调用了 **WSAAsyncSelect()** 或 **WSAEventSelect()** 函数后,再调用 **ioctlsocket()** 函数将该 socket 设置为非阻塞模式会失败,你必须先调用 **WSAAsyncSelect()** 通过将 **lEvent** 参数为 **0** 或调用 **WSAEventSelect()** 通过设置 **lNetworkEvents** 参数为 **0** 来清除已经设置的 socket 相关标志位,再次调用 **ioctlsocket()** 将该 socket 设置成阻塞模式才会成功。因为调用 **WSAAsyncSelect()** 或**WSAEventSelect()** 函数会自动将 socket 设置成非阻塞模式。MSDN 上原文(https://docs.microsoft.com/en-us/windows/desktop/api/winsock/nf-winsock-ioctlsocket)如下: 92 | 93 | ``` 94 | The WSAAsyncSelect and WSAEventSelect functions automatically set a socket to nonblocking mode. If WSAAsyncSelect or WSAEventSelect has been issued on a socket, then any attempt to use ioctlsocket to set the socket back to blocking mode will fail with WSAEINVAL. 95 | 96 | To set the socket back to blocking mode, an application must first disable WSAAsyncSelect by calling WSAAsyncSelect with the lEvent parameter equal to zero, or disable WSAEventSelect by calling WSAEventSelect with the lNetworkEvents parameter equal to zero. 97 | ``` 98 | 99 | 100 | 101 | 关于 **WSAAsyncSelect()** 和 **WSAEventSelect()** 这两个函数,后文中会详细讲解。 102 | 103 | 104 | 105 | > 注意事项:无论是 Linux 的 fcntl 函数,还是 Windows 的 ioctlsocket,建议读者在实际编码中判断一下函数返回值以确定是否调用成功。 106 | 107 | 108 | 109 | 110 | 111 | ------ 112 | 113 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 114 | 115 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 116 | 117 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /code/WSAEventSelect/WSAEventSelect.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | 14 | {7D04DFA2-70C2-4F25-AC66-F0F587648169} 15 | Win32Proj 16 | WSAEventSelect 17 | 18 | 19 | 20 | Application 21 | true 22 | v120 23 | Unicode 24 | 25 | 26 | Application 27 | false 28 | v120 29 | true 30 | Unicode 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | true 44 | 45 | 46 | false 47 | 48 | 49 | 50 | Use 51 | Level3 52 | Disabled 53 | WIN32;_DEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) 54 | true 55 | 56 | 57 | Console 58 | true 59 | 60 | 61 | 62 | 63 | Level3 64 | Use 65 | MaxSpeed 66 | true 67 | true 68 | WIN32;NDEBUG;_CONSOLE;_LIB;%(PreprocessorDefinitions) 69 | true 70 | 71 | 72 | Console 73 | true 74 | true 75 | true 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | Create 88 | Create 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /code/WSAAsyncSelect/WSAAsyncSelect.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | 14 | {87BE6464-6DE0-484B-8E81-57C8AB0C84CB} 15 | Win32Proj 16 | WSAAsyncSelect 17 | 18 | 19 | 20 | Application 21 | true 22 | v120 23 | Unicode 24 | 25 | 26 | Application 27 | false 28 | v120 29 | true 30 | Unicode 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | true 44 | 45 | 46 | false 47 | 48 | 49 | 50 | Use 51 | Level3 52 | Disabled 53 | WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions) 54 | 55 | 56 | Windows 57 | true 58 | 59 | 60 | 61 | 62 | Level3 63 | Use 64 | MaxSpeed 65 | true 66 | true 67 | WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions) 68 | 69 | 70 | Windows 71 | true 72 | true 73 | true 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | Create 88 | Create 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 网络通信基础重难点解析 专题介绍 2 | 3 | 不积跬步无以至千里,不积小流无以成江海。 4 | 5 | 当我们了解了网络通信的基本原理后,你需要实际去编写一些网络通信程序,随着技术的更新换代、大浪淘沙,目前主要的网络通信技术都是基于 TCP/IP 协议栈的,对应到应用层的编码来说就是使用操作系统提供的 socket API 来编写网络通信程序。然而遗憾的是,拜各种网络库和开发 IDE 所赐,很多开发者或者网络编程的初学者都忽视了对这些基础的 socket API 的掌握。殊不知,操作系统层面提供的 API 会在相当长的时间内保持接口不变,一旦学成,终生受用。理解和掌握这些基础 socket API 不仅可以最大化地去定制各种网络通信框架,更不用说使用市面上流行的网络通信库了,最重要的是,它会是你排查各种网络疑难杂症坚实的技术保障。 6 | 7 | 8 | 9 | ### 文章目录 10 | 11 | [网络通信基础重难点解析 01:常用 socket 函数基础](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2001%EF%BC%9A%E5%B8%B8%E7%94%A8%20socket%20%E5%87%BD%E6%95%B0%E5%9F%BA%E7%A1%80.md) 12 | 13 | [网络通信基础重难点解析 02:TCP 通信基本流程](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2002%EF%BC%9ATCP%20%E9%80%9A%E4%BF%A1%E5%9F%BA%E6%9C%AC%E6%B5%81%E7%A8%8B.md) 14 | 15 | [网络通信基础重难点解析 03:bind 函数](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2003%EF%BC%9Abind%20%E5%87%BD%E6%95%B0.md) 16 | 17 | [网络通信基础重难点解析 04 :select 函数用法](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2004%20%EF%BC%9Aselect%20%E5%87%BD%E6%95%B0%E7%94%A8%E6%B3%95.md) 18 | 19 | [网络通信基础重难点解析 05 :socket 的阻塞模式和非阻塞模式](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2005%20%EF%BC%9Asocket%20%E7%9A%84%E9%98%BB%E5%A1%9E%E6%A8%A1%E5%BC%8F%E5%92%8C%E9%9D%9E%E9%98%BB%E5%A1%9E%E6%A8%A1%E5%BC%8F.md) 20 | 21 | [网络通信基础重难点解析 06 :send 和 recv 函数在阻塞和非阻塞模式下的行为](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2006%20%EF%BC%9Asend%20%E5%92%8C%20recv%20%E5%87%BD%E6%95%B0%E5%9C%A8%E9%98%BB%E5%A1%9E%E5%92%8C%E9%9D%9E%E9%98%BB%E5%A1%9E%E6%A8%A1%E5%BC%8F%E4%B8%8B%E7%9A%84%E8%A1%8C%E4%B8%BA.md) 22 | 23 | [网络通信基础重难点解析 07 :非阻塞模式下 send 和 recv 函数的返回值总结](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2007%20%EF%BC%9A%E9%9D%9E%E9%98%BB%E5%A1%9E%E6%A8%A1%E5%BC%8F%E4%B8%8B%20send%20%E5%92%8C%20recv%20%E5%87%BD%E6%95%B0%E7%9A%84%E8%BF%94%E5%9B%9E%E5%80%BC%E6%80%BB%E7%BB%93.md) 24 | 25 | [网络通信基础重难点解析 08 :connect 函数在阻塞和非阻塞模式下的行为](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2008%20%EF%BC%9Aconnect%20%E5%87%BD%E6%95%B0%E5%9C%A8%E9%98%BB%E5%A1%9E%E5%92%8C%E9%9D%9E%E9%98%BB%E5%A1%9E%E6%A8%A1%E5%BC%8F%E4%B8%8B%E7%9A%84%E8%A1%8C%E4%B8%BA.md) 26 | 27 | [网络通信基础重难点解析 09 :阻塞与非阻塞的 socket 的各自适用场景](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2009%20%EF%BC%9A%E9%98%BB%E5%A1%9E%E4%B8%8E%E9%9D%9E%E9%98%BB%E5%A1%9E%E7%9A%84%20socket%20%E7%9A%84%E5%90%84%E8%87%AA%E9%80%82%E7%94%A8%E5%9C%BA%E6%99%AF.md) 28 | 29 | [网络通信基础重难点解析 10 :Linux EINTR 错误码](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2010%20%EF%BC%9ALinux%20EINTR%20%E9%94%99%E8%AF%AF%E7%A0%81.md) 30 | 31 | [网络通信基础重难点解析 11 :Linux poll 函数用法](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2011%20%EF%BC%9ALinux%20poll%20%E5%87%BD%E6%95%B0%E7%94%A8%E6%B3%95.md) 32 | 33 | [网络通信基础重难点解析 12 :Linux epoll 模型](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2012%20%EF%BC%9ALinux%20epoll%20%E6%A8%A1%E5%9E%8B.md) 34 | 35 | [网络通信基础重难点解析 13 :Windows WSAEventSelect 网络通信模型](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2013%20%EF%BC%9AWindows%20WSAEventSelect%20%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E6%A8%A1%E5%9E%8B.md) 36 | 37 | [网络通信基础重难点解析 14 :Windows 的 WSAAsyncSelect 网络通信模型](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2014%20%EF%BC%9AWindows%20%E7%9A%84%20WSAAsyncSelect%20%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E6%A8%A1%E5%9E%8B.md) 38 | 39 | [网络通信基础重难点解析 15 :主机字节序和网络字节序](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2015%20%EF%BC%9A%E4%B8%BB%E6%9C%BA%E5%AD%97%E8%8A%82%E5%BA%8F%E5%92%8C%E7%BD%91%E7%BB%9C%E5%AD%97%E8%8A%82%E5%BA%8F.md) 40 | 41 | [网络通信基础重难点解析 16 :域名解析 API 介绍](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2016%20%EF%BC%9A%E5%9F%9F%E5%90%8D%E8%A7%A3%E6%9E%90%20API%20%E4%BB%8B%E7%BB%8D.md) 42 | 43 | [网络通信基础重难点解析 17 :Windows 完成端口(IOCP)模型重难点解析](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2017%20%EF%BC%9AWindows%20%E5%AE%8C%E6%88%90%E7%AB%AF%E5%8F%A3%EF%BC%88IOCP%EF%BC%89%E6%A8%A1%E5%9E%8B%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90.md) 44 | 45 | [网络通信基础重难点解析 18: IOCP实例 - gh0st源码分析(以网络通信模块为重点)](https://github.com/balloonwj/easyserverdev/blob/master/%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1%E5%9F%BA%E7%A1%80%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90%2017%20%EF%BC%9AWindows%20%E5%AE%8C%E6%88%90%E7%AB%AF%E5%8F%A3%EF%BC%88IOCP%EF%BC%89%E6%A8%A1%E5%9E%8B%E9%87%8D%E9%9A%BE%E7%82%B9%E8%A7%A3%E6%9E%90.md) 46 | 47 | 48 | 49 | 50 | 51 | ------ 52 | 53 | **本专题文章来源于『easyserverdev』公众号,欢迎关注,转载或 fork 请保留版权信息。** 54 | 55 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 56 | 57 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) 58 | -------------------------------------------------------------------------------- /网络通信基础重难点解析 02:TCP 通信基本流程.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 02:TCP 通信基本流程 2 | 3 | 4 | 5 | ### TCP 通信基本流程 6 | 7 | 不管多么复杂的服务器或客户端程序,其网络通信的基本原理一定如下所述: 8 | 9 | 对于服务器,其通信流程一般有如下步骤: 10 | 11 | ``` 12 | 1. 调用 socket 函数创建 socket(侦听socket) 13 | 2. 调用 bind 函数 将 socket绑定到某个ip和端口的二元组上 14 | 3. 调用 listen 函数 开启侦听 15 | 4. 当有客户端请求连接上来后,调用 accept 函数接受连接,产生一个新的 socket(客户端 socket) 16 | 5. 基于新产生的 socket 调用 send 或 recv 函数开始与客户端进行数据交流 17 | 6. 通信结束后,调用 close 函数关闭侦听 socket 18 | ``` 19 | 20 | 对于客户端,其通信流程一般有如下步骤: 21 | 22 | ``` 23 | 1. 调用 socket函数创建客户端 socket 24 | 2. 调用 connect 函数尝试连接服务器 25 | 3. 连接成功以后调用 send 或 recv 函数开始与服务器进行数据交流 26 | 4. 通信结束后,调用 close 函数关闭侦听socket 27 | ``` 28 | 29 | 上述流程可以绘制成如下图示: 30 | 31 | ![20181213192725.png](http://www.hootina.org/github_easyserverdev/20181213192725.png) 32 | 33 | 34 | 35 | 对于上面的图,读者可能有疑问,为什么客户端调用 **close()** ,会和服务器端 **recv()** 函数有关。这个涉及到 **recv()** 函数的返回值意义,我们在下文中详细讲解。 36 | 37 | 服务器端实现代码: 38 | 39 | ``` 40 | /** 41 | * TCP服务器通信基本流程 42 | * zhangyl 2018.12.13 43 | */ 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | 51 | int main(int argc, char* argv[]) 52 | { 53 | //1.创建一个侦听socket 54 | int listenfd = socket(AF_INET, SOCK_STREAM, 0); 55 | if (listenfd == -1) 56 | { 57 | std::cout << "create listen socket error." << std::endl; 58 | return -1; 59 | } 60 | 61 | //2.初始化服务器地址 62 | struct sockaddr_in bindaddr; 63 | bindaddr.sin_family = AF_INET; 64 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 65 | bindaddr.sin_port = htons(3000); 66 | if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 67 | { 68 | std::cout << "bind listen socket error." << std::endl; 69 | return -1; 70 | } 71 | 72 | //3.启动侦听 73 | if (listen(listenfd, SOMAXCONN) == -1) 74 | { 75 | std::cout << "listen error." << std::endl; 76 | return -1; 77 | } 78 | 79 | while (true) 80 | { 81 | struct sockaddr_in clientaddr; 82 | socklen_t clientaddrlen = sizeof(clientaddr); 83 | //4. 接受客户端连接 84 | int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); 85 | if (clientfd != -1) 86 | { 87 | char recvBuf[32] = {0}; 88 | //5. 从客户端接受数据 89 | int ret = recv(clientfd, recvBuf, 32, 0); 90 | if (ret > 0) 91 | { 92 | std::cout << "recv data from client, data: " << recvBuf << std::endl; 93 | //6. 将收到的数据原封不动地发给客户端 94 | ret = send(clientfd, recvBuf, strlen(recvBuf), 0); 95 | if (ret != strlen(recvBuf)) 96 | std::cout << "send data error." << std::endl; 97 | 98 | std::cout << "send data to client successfully, data: " << recvBuf << std::endl; 99 | } 100 | else 101 | { 102 | std::cout << "recv data error." << std::endl; 103 | } 104 | 105 | close(clientfd); 106 | } 107 | } 108 | 109 | //7.关闭侦听socket 110 | close(listenfd); 111 | 112 | return 0; 113 | } 114 | ``` 115 | 116 | 客户端实现代码: 117 | 118 | ``` 119 | /** 120 | * TCP客户端通信基本流程 121 | * zhangyl 2018.12.13 122 | */ 123 | #include 124 | #include 125 | #include 126 | #include 127 | #include 128 | #include 129 | 130 | #define SERVER_ADDRESS "127.0.0.1" 131 | #define SERVER_PORT 3000 132 | #define SEND_DATA "helloworld" 133 | 134 | int main(int argc, char* argv[]) 135 | { 136 | //1.创建一个socket 137 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 138 | if (clientfd == -1) 139 | { 140 | std::cout << "create client socket error." << std::endl; 141 | return -1; 142 | } 143 | 144 | //2.连接服务器 145 | struct sockaddr_in serveraddr; 146 | serveraddr.sin_family = AF_INET; 147 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 148 | serveraddr.sin_port = htons(SERVER_PORT); 149 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 150 | { 151 | std::cout << "connect socket error." << std::endl; 152 | return -1; 153 | } 154 | 155 | //3. 向服务器发送数据 156 | int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); 157 | if (ret != strlen(SEND_DATA)) 158 | { 159 | std::cout << "send data error." << std::endl; 160 | return -1; 161 | } 162 | 163 | std::cout << "send data successfully, data: " << SEND_DATA << std::endl; 164 | 165 | //4. 从客户端收取数据 166 | char recvBuf[32] = {0}; 167 | ret = recv(clientfd, recvBuf, 32, 0); 168 | if (ret > 0) 169 | { 170 | std::cout << "recv data successfully, data: " << recvBuf << std::endl; 171 | } 172 | else 173 | { 174 | std::cout << "recv data error, data: " << recvBuf << std::endl; 175 | } 176 | 177 | //5. 关闭socket 178 | close(clientfd); 179 | 180 | return 0; 181 | } 182 | ``` 183 | 184 | 185 | 186 | 以上代码,服务器端在地址 **0.0.0.0:3000** 启动一个侦听,客户端连接服务器成功后,给服务器发送字符串"helloworld";服务器收到后,将收到的字符串原封不动地发给客户端。 187 | 188 | 在 Linux Shell 界面输入以下命令编译服务器端和客户端: 189 | 190 | ``` 191 | # 编译 server.cpp 生成可执行文件 server 192 | [root@localhost testsocket]# g++ -g -o server server.cpp 193 | # 编译 client.cpp 生成可执行文件 client 194 | [root@localhost testsocket]# g++ -g -o client client.cpp 195 | ``` 196 | 197 | 接着,我们看下执行效果,先启动服务器程序: 198 | 199 | ``` 200 | [root@localhost testsocket]# ./server 201 | ``` 202 | 203 | 再启动客户端程序: 204 | 205 | ``` 206 | [root@localhost testsocket]# ./client 207 | ``` 208 | 209 | 这个时候客户端输出: 210 | 211 | ``` 212 | send data successfully, data: helloworld 213 | recv data successfully, data: helloworld 214 | ``` 215 | 216 | 服务器端输出: 217 | 218 | ``` 219 | recv data from client, data: helloworld 220 | send data to client successfully, data: helloworld 221 | ``` 222 | 223 | 224 | 225 | 以上就是 TCP socket 网络通信的基本原理,对于很多读者来说,上述代码可能很简单,更有点“玩具”的意味。但是深刻理解这两个代码片段是进一步学习开发复杂的网络通信程序的基础。而且看似很简单的代码,却隐藏了很多的玄机和原理,接下来的章节我们将以这两段代码为蓝本,逐渐深入。 226 | 227 | 228 | 229 | 230 | 231 | ------ 232 | 233 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 234 | 235 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 236 | 237 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) 238 | 239 | 240 | -------------------------------------------------------------------------------- /网络通信基础重难点解析 08 :connect 函数在阻塞和非阻塞模式下的行为.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 08 :connect 函数在阻塞和非阻塞模式下的行为 2 | 3 | 4 | 5 | #### connect 函数在阻塞和非阻塞模式下的行为 6 | 7 | 在 socket 是阻塞模式下 connect 函数会一直到有明确的结果才会返回(或连接成功或连接失败),如果服务器地址“较远”,连接速度比较慢,connect 函数在连接过程中可能会导致程序阻塞在 connect 函数处好一会儿(如两三秒之久),虽然这一般也不会对依赖于网络通信的程序造成什么影响,但在实际项目中,我们一般倾向使用所谓的**异步的 connect** 技术,或者叫**非阻塞的 connect**。这个流程一般有如下步骤: 8 | 9 | ``` 10 | 1. 创建socket,并将 socket 设置成非阻塞模式; 11 | 2. 调用 connect 函数,此时无论 connect 函数是否连接成功会立即返回;如果返回 -1 并不一定表示连接出错,如果此时错误码是EINPROGRESS,则表示正在尝试连接; 12 | 3. 接着调用 select 函数,在指定的时间内判断该 socket 是否可写,如果可写说明连接成功,反之则认为连接失败。 13 | ``` 14 | 15 | 按上述流程编写代码如下: 16 | 17 | ``` 18 | /** 19 | * 异步的connect写法,nonblocking_connect.cpp 20 | * zhangyl 2018.12.17 21 | */ 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #define SERVER_ADDRESS "127.0.0.1" 33 | #define SERVER_PORT 3000 34 | #define SEND_DATA "helloworld" 35 | 36 | int main(int argc, char* argv[]) 37 | { 38 | //1.创建一个socket 39 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 40 | if (clientfd == -1) 41 | { 42 | std::cout << "create client socket error." << std::endl; 43 | return -1; 44 | } 45 | 46 | //将 clientfd 设置成非阻塞模式 47 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 48 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 49 | if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 50 | { 51 | close(clientfd); 52 | std::cout << "set socket to nonblock error." << std::endl; 53 | return -1; 54 | } 55 | 56 | //2.连接服务器 57 | struct sockaddr_in serveraddr; 58 | serveraddr.sin_family = AF_INET; 59 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 60 | serveraddr.sin_port = htons(SERVER_PORT); 61 | for (;;) 62 | { 63 | int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); 64 | if (ret == 0) 65 | { 66 | std::cout << "connect to server successfully." << std::endl; 67 | close(clientfd); 68 | return 0; 69 | } 70 | else if (ret == -1) 71 | { 72 | if (errno == EINTR) 73 | { 74 | //connect 动作被信号中断,重试connect 75 | std::cout << "connecting interruptted by signal, try again." << std::endl; 76 | continue; 77 | } else if (errno == EINPROGRESS) 78 | { 79 | //连接正在尝试中 80 | break; 81 | } else { 82 | //真的出错了, 83 | close(clientfd); 84 | return -1; 85 | } 86 | } 87 | } 88 | 89 | fd_set writeset; 90 | FD_ZERO(&writeset); 91 | FD_SET(clientfd, &writeset); 92 | //可以利用tv_sec和tv_usec做更小精度的超时控制 93 | struct timeval tv; 94 | tv.tv_sec = 3; 95 | tv.tv_usec = 0; 96 | if (select(clientfd + 1, NULL, &writeset, NULL, &tv) == 1) 97 | { 98 | std::cout << "[select] connect to server successfully." << std::endl; 99 | } else { 100 | std::cout << "[select] connect to server error." << std::endl; 101 | } 102 | 103 | //5. 关闭socket 104 | close(clientfd); 105 | 106 | return 0; 107 | } 108 | ``` 109 | 110 | 为了区别到底是在调用 connect 函数时判断连接成功还是通过 select 函数判断连接成功,我们在后者的输出内容中加上了“**[select]**”标签以示区别。 111 | 112 | 我们先用 **nc** 命令启动一个服务器程序: 113 | 114 | ``` 115 | nc -v -l 0.0.0.0 3000 116 | ``` 117 | 118 | 然后编译客户端程序并执行: 119 | 120 | ``` 121 | [root@localhost testsocket]# g++ -g -o nonblocking_connect nonblocking_connect.cpp 122 | [root@localhost testsocket]# ./nonblocking_connect 123 | [select] connect to server successfully. 124 | ``` 125 | 126 | 我们把服务器程序关掉,再重新启动一下客户端,这个时候应该会连接失败,程序输出结果如下: 127 | 128 | ``` 129 | [root@localhost testsocket]# ./nonblocking_connect 130 | [select] connect to server successfully. 131 | ``` 132 | 133 | 奇怪?为什么连接不上也会得出一样的输出结果?难道程序有问题?这是因为: 134 | 135 | - 在 Windows 系统上,一个 socket 没有建立连接之前,我们使用 select 函数检测其是否可写,能得到正确的结果(不可写),连接成功后检测,会变为可写。所以,上述介绍的异步 **connect** 写法流程在 Windows 系统上时没有问题的。 136 | 137 | - 在 Linux 系统上一个 socket 没有建立连接之前,用 select 函数检测其是否可写,你也会得到可写的结果,所以上述流程并不适用于 Linux 系统。正确的做法是,connect 之后,不仅要用 **select** 检测可写,还要检测此时 socket 是否出错,通过错误码来检测确定是否连接上,错误码为 0 表示连接上,反之为未连接上。完整代码如下: 138 | 139 | ``` 140 | /** 141 | * Linux 下正确的异步的connect写法,linux_nonblocking_connect.cpp 142 | * zhangyl 2018.12.17 143 | */ 144 | #include 145 | #include 146 | #include 147 | #include 148 | #include 149 | #include 150 | #include 151 | #include 152 | #include 153 | 154 | #define SERVER_ADDRESS "127.0.0.1" 155 | #define SERVER_PORT 3000 156 | #define SEND_DATA "helloworld" 157 | 158 | int main(int argc, char* argv[]) 159 | { 160 | //1.创建一个socket 161 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 162 | if (clientfd == -1) 163 | { 164 | std::cout << "create client socket error." << std::endl; 165 | return -1; 166 | } 167 | 168 | //将 clientfd 设置成非阻塞模式 169 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 170 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 171 | if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 172 | { 173 | close(clientfd); 174 | std::cout << "set socket to nonblock error." << std::endl; 175 | return -1; 176 | } 177 | 178 | //2.连接服务器 179 | struct sockaddr_in serveraddr; 180 | serveraddr.sin_family = AF_INET; 181 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 182 | serveraddr.sin_port = htons(SERVER_PORT); 183 | for (;;) 184 | { 185 | int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); 186 | if (ret == 0) 187 | { 188 | std::cout << "connect to server successfully." << std::endl; 189 | close(clientfd); 190 | return 0; 191 | } 192 | else if (ret == -1) 193 | { 194 | if (errno == EINTR) 195 | { 196 | //connect 动作被信号中断,重试connect 197 | std::cout << "connecting interruptted by signal, try again." << std::endl; 198 | continue; 199 | } else if (errno == EINPROGRESS) 200 | { 201 | //连接正在尝试中 202 | break; 203 | } else { 204 | //真的出错了, 205 | close(clientfd); 206 | return -1; 207 | } 208 | } 209 | } 210 | 211 | fd_set writeset; 212 | FD_ZERO(&writeset); 213 | FD_SET(clientfd, &writeset); 214 | //可以利用tv_sec和tv_usec做更小精度的超时控制 215 | struct timeval tv; 216 | tv.tv_sec = 3; 217 | tv.tv_usec = 0; 218 | if (select(clientfd + 1, NULL, &writeset, NULL, &tv) != 1) 219 | { 220 | std::cout << "[select] connect to server error." << std::endl; 221 | close(clientfd); 222 | return -1; 223 | } 224 | 225 | int err; 226 | socklen_t len = static_cast(sizeof err); 227 | if (::getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) 228 | { 229 | close(clientfd); 230 | return -1; 231 | } 232 | 233 | if (err == 0) 234 | std::cout << "connect to server successfully." << std::endl; 235 | else 236 | std::cout << "connect to server error." << std::endl; 237 | 238 | //5. 关闭socket 239 | close(clientfd); 240 | 241 | return 0; 242 | } 243 | ``` 244 | 245 | 246 | 247 | > 当然,在实际的项目中,第 3 个步骤中 Linux 平台上你也可以使用 **poll** 函数来判断 socket 是否可写;在 Windows 平台上你可以使用 **WSAEventSelect** 或 **WSAAsyncSelect** 函数判断连接是否成功,关于这三个函数我们将在后面的章节中详细讲解,这里暂且仅以 **select** 函数为例。 248 | 249 | 250 | 251 | ------ 252 | 253 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 254 | 255 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 256 | 257 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /网络通信基础重难点解析 14 :Windows 的 WSAAsyncSelect 网络通信模型.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 14 :Windows 的 WSAAsyncSelect 网络通信模型 2 | 3 | 4 | 5 | ### Windows 的 WSAAsyncSelect 网络通信模型 6 | 7 | **WSAAsyncSelect ** 是 Windows 系统非常常用一个网络通信模型,它的原理是将 socket 句柄绑定到一个 Windows 窗口上并利于 Windows 的窗口消息机制实现了网络有消息时调用窗口函数。**WSAAsyncSelect ** 函数签名如下: 8 | 9 | ``` 10 | int WSAAsyncSelect( 11 | SOCKET s, 12 | HWND hWnd, 13 | u_int wMsg, 14 | long lEvent 15 | ); 16 | ``` 17 | 18 | 参数 **s** 和 **hwnd** 是需要绑定在一起的 socket 句柄和窗口句柄,参数 **uMsg** 是自定义的一个窗口消息,socket 有事件时会产生这个消息类型,为了避免与 Windows 内置消息冲突,通常这个消息值应该在 WM_USER 基础之上定义(如 WM_USER + 1),参数 **lEvent** 即 要监听的 socket 事件类型,它的取值是上一小节介绍的 FD_XXX 系列。函数调用成功返回 0 值,调用失败返回 SOCKET_ERROR(-1)。 19 | 20 | > **WSAAsyncSelect** 如果设置了 lEvent 值(非 0),会自动将参数 s 对应的 socket 设置为非阻塞模式;反之,如果设置 lEvent = 0 会自动将 socket 变回阻塞模式。 21 | 22 | 23 | 24 | 我们来看一个具体的示例代码: 25 | 26 | ``` 27 | // WSAAsyncSelect.cpp : Defines the entry point for the application. 28 | // 29 | 30 | #include "stdafx.h" 31 | #include 32 | #include "WSAAsyncSelect.h" 33 | 34 | #pragma comment(lib, "ws2_32.lib") 35 | 36 | //socket 消息 37 | #define WM_SOCKET WM_USER + 1 38 | 39 | //当前在线用户数量 40 | int g_nCount = 0; 41 | 42 | SOCKET InitSocket(); 43 | ATOM MyRegisterClass(HINSTANCE hInstance); 44 | HWND InitInstance(HINSTANCE hInstance, int nCmdShow); 45 | LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); 46 | LRESULT OnSocketEvent(HWND hWnd, WPARAM wParam, LPARAM lParam); 47 | 48 | int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) 49 | { 50 | UNREFERENCED_PARAMETER(hPrevInstance); 51 | UNREFERENCED_PARAMETER(lpCmdLine); 52 | 53 | SOCKET hListenSocket = InitSocket(); 54 | if (hListenSocket == INVALID_SOCKET) 55 | return 1; 56 | 57 | MSG msg; 58 | MyRegisterClass(hInstance); 59 | 60 | HWND hwnd = InitInstance(hInstance, nCmdShow); 61 | if (hwnd == NULL) 62 | return 1; 63 | 64 | //利用 WSAAsyncSelect 将侦听 socket 与 hwnd 绑定在一起 65 | if (WSAAsyncSelect(hListenSocket, hwnd, WM_SOCKET, FD_ACCEPT) == SOCKET_ERROR) 66 | return 1; 67 | 68 | while (GetMessage(&msg, NULL, 0, 0)) 69 | { 70 | TranslateMessage(&msg); 71 | DispatchMessage(&msg); 72 | } 73 | 74 | closesocket(hListenSocket); 75 | WSACleanup(); 76 | 77 | return (int) msg.wParam; 78 | } 79 | 80 | SOCKET InitSocket() 81 | { 82 | //1. 初始化套接字库 83 | WORD wVersionRequested; 84 | WSADATA wsaData; 85 | wVersionRequested = MAKEWORD(1, 1); 86 | int nError = WSAStartup(wVersionRequested, &wsaData); 87 | if (nError != 0) 88 | return INVALID_SOCKET; 89 | 90 | if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) 91 | { 92 | WSACleanup(); 93 | return INVALID_SOCKET; 94 | } 95 | 96 | //2. 创建用于监听的套接字 97 | SOCKET hListenSocket = socket(AF_INET, SOCK_STREAM, 0); 98 | SOCKADDR_IN addrSrv; 99 | addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); 100 | addrSrv.sin_family = AF_INET; 101 | addrSrv.sin_port = htons(6000); 102 | 103 | //3. 绑定套接字 104 | if (bind(hListenSocket, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR) 105 | { 106 | closesocket(hListenSocket); 107 | WSACleanup(); 108 | return INVALID_SOCKET; 109 | } 110 | 111 | //4. 将套接字设为监听模式,准备接受客户请求 112 | if (listen(hListenSocket, SOMAXCONN) == SOCKET_ERROR) 113 | { 114 | closesocket(hListenSocket); 115 | WSACleanup(); 116 | return INVALID_SOCKET; 117 | } 118 | 119 | return hListenSocket; 120 | } 121 | 122 | ATOM MyRegisterClass(HINSTANCE hInstance) 123 | { 124 | WNDCLASSEX wcex; 125 | 126 | wcex.cbSize = sizeof(WNDCLASSEX); 127 | 128 | wcex.style = CS_HREDRAW | CS_VREDRAW; 129 | wcex.lpfnWndProc = WndProc; 130 | wcex.cbClsExtra = 0; 131 | wcex.cbWndExtra = 0; 132 | wcex.hInstance = hInstance; 133 | wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WSAASYNCSELECT)); 134 | wcex.hCursor = LoadCursor(NULL, IDC_ARROW); 135 | wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); 136 | wcex.lpszMenuName = NULL; 137 | wcex.lpszClassName = _T("DemoWindowCls"); 138 | wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); 139 | 140 | return RegisterClassEx(&wcex); 141 | } 142 | 143 | HWND InitInstance(HINSTANCE hInstance, int nCmdShow) 144 | { 145 | HWND hWnd = CreateWindow(_T("DemoWindowCls"), _T("DemoWindow"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL); 146 | if (!hWnd) 147 | return NULL; 148 | 149 | ShowWindow(hWnd, nCmdShow); 150 | UpdateWindow(hWnd); 151 | 152 | return hWnd; 153 | } 154 | 155 | LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 156 | { 157 | int wmId, wmEvent; 158 | PAINTSTRUCT ps; 159 | HDC hdc; 160 | 161 | switch (uMsg) 162 | { 163 | case WM_SOCKET: 164 | return OnSocketEvent(hWnd, wParam, lParam); 165 | 166 | 167 | case WM_PAINT: 168 | hdc = BeginPaint(hWnd, &ps); 169 | // TODO: Add any drawing code here... 170 | EndPaint(hWnd, &ps); 171 | break; 172 | 173 | case WM_DESTROY: 174 | PostQuitMessage(0); 175 | break; 176 | default: 177 | return DefWindowProc(hWnd, uMsg, wParam, lParam); 178 | } 179 | 180 | return 0; 181 | } 182 | 183 | LRESULT OnSocketEvent(HWND hWnd, WPARAM wParam, LPARAM lParam) 184 | { 185 | SOCKET s = (SOCKET)wParam; 186 | int nEventType = WSAGETSELECTEVENT(lParam); 187 | int nErrorCode = WSAGETSELECTERROR(lParam); 188 | if (nErrorCode != 0) 189 | return 1; 190 | 191 | switch (nEventType) 192 | { 193 | case FD_ACCEPT: 194 | { 195 | //调用accept函数处理接受连接事件; 196 | SOCKADDR_IN addrClient; 197 | int len = sizeof(SOCKADDR); 198 | //等待客户请求到来 199 | SOCKET hSockClient = accept(s, (SOCKADDR*)&addrClient, &len); 200 | if (hSockClient != SOCKET_ERROR) 201 | { 202 | //产生的客户端socket,监听其 FD_READ/FD_CLOSE 事件 203 | if (WSAAsyncSelect(hSockClient, hWnd, WM_SOCKET, FD_READ | FD_CLOSE) == SOCKET_ERROR) 204 | { 205 | closesocket(hSockClient); 206 | return 1; 207 | } 208 | 209 | g_nCount++; 210 | TCHAR szLogMsg[64]; 211 | wsprintf(szLogMsg, _T("a client connected, socket: %d, current: %d\n"), (int)hSockClient, g_nCount); 212 | OutputDebugString(szLogMsg); 213 | } 214 | } 215 | break; 216 | 217 | case FD_READ: 218 | { 219 | char szBuf[64] = { 0 }; 220 | int n = recv(s, szBuf, 64, 0); 221 | if (n > 0) 222 | { 223 | OutputDebugStringA(szBuf); 224 | } 225 | else if (n <= 0) 226 | { 227 | g_nCount--; 228 | TCHAR szLogMsg[64]; 229 | wsprintf(szLogMsg, _T("a client disconnected, socket: %d, current: %d\n"), (int)s, g_nCount); 230 | OutputDebugString(szLogMsg); 231 | closesocket(s); 232 | } 233 | } 234 | break; 235 | 236 | case FD_CLOSE: 237 | { 238 | g_nCount--; 239 | TCHAR szLogMsg[64]; 240 | wsprintf(szLogMsg, _T("a client disconnected, socket: %d, current: %d\n"), (int)s, g_nCount); 241 | OutputDebugString(szLogMsg); 242 | closesocket(s); 243 | } 244 | break; 245 | 246 | }// end switch 247 | 248 | return 0; 249 | } 250 | ``` 251 | 252 | 在 Visual Studio 中编译该程序,然后在另外一台 Linux 机器上使用 nc 命令模拟几个客户端,模拟命令如下: 253 | 254 | ``` 255 | # 我的服务器地址是 192.168.1.131 256 | [root@localhost ~]# nc -v 192.168.1.131 6000 257 | ``` 258 | 259 | Windows 服务程序的输出是使用 OutputDebugString 函数来输出到 Visual Studio 的 **Output 窗口**中去的,所以需要在调试模式下运行服务程序。**Output 窗口** 输出效果如下: 260 | 261 | ![](http://www.hootina.org/github_easyserverdev/20190318221652.png) 262 | 263 | 264 | 265 | 上述代码中有几个地方需要注意: 266 | 267 | - 当产生了 WM_SOCKET 消息时,消息携带的参数 wParam 的值是产生网络事件的 socket 句柄值,参数 lParam 分为两段,高 16 位(bit)(2 字节)是网络错误码(0 为没有错位),低 16 位(bit)(2 字节)是网络事件类型,Windows 专门为了取得这两个值分别定义了宏 **WSAGETSELECTERROR** 和 **WSAGETSELECTEVENT**。 268 | 269 | ``` 270 | #define WSAGETSELECTEVENT(lParam) LOWORD(lParam) 271 | #define WSAGETSELECTERROR(lParam) HIWORD(lParam) 272 | ``` 273 | 274 | - 对于侦听 socket, 我们这里只关注其 FD_CONNECT 事件,对于普通 socket 我们关注其 FD_READ 和 FD_CLOSE 事件。 275 | 276 | 277 | 278 | > mfc 中的 CAsyncSocket 类的实现就是基于 WSAAsyncSelect 这个函数封装的。 279 | 280 | 281 | 282 | ------ 283 | 284 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 285 | 286 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 287 | 288 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /网络通信基础重难点解析 01:常用 socket 函数基础.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 01:常用 socket 函数基础 2 | 3 | 4 | 5 | ### 常用 socket 函数基础 6 | 7 | Windows 和 Linux 上常用的 socket API 函数并不多,除了特定操作系统提供的一些基于自身系统特性的 API, 大多数 Socket API 都源于**BSD Socket** (即**伯克利套接字**(**Berkeley Sockets**)),因此这些 socket 函数在不同的平台有着相似的签名和参数。 8 | 9 | 经常有想学习网络编程的新人询问要掌握哪些基础的socket API,我这里给一个简单的函数列表,列表中给出的都是应该熟练掌握的 socket 函数。 10 | 11 | ​ 常用 Berkeley Sockets API 一览表 12 | 13 | | 函数名称 | 函数简单描述 | 附加说明 | 14 | | :-----------: | :--------------------------------------: | :--------------------------------: | 15 | | socket | 创造某种类型的套接字 | | 16 | | bind | 将一个 socket 绑定一个ip与端口的二元组上 | | 17 | | listen | 将一个 socket 变为侦听状态 | | 18 | | connect | 试图建立一个 TCP 连接 | 一般用于客户端 | 19 | | accept | 尝试接收一个连接 | 一般用于服务端 | 20 | | send | 通过一个socket发送数据 | | 21 | | recv | 通过一个socket收取数据 | | 22 | | select | 判断一组socket上的读事件 | | 23 | | gethostbyname | 通过域名获取机器地址 | | 24 | | close | 关闭一个套接字,回收该 socket 对应的资源 | Windows 系统中对应的是 closesocket | 25 | | shutdown | 关闭 socket 收或发通道 | | 26 | | setsockopt | 设置一个套接字选项 | | 27 | | getsockopt | 获取一个套接字选项 | | 28 | 29 | 对于某个 socket 函数,如果你想查看它的用法,可以通过相应的帮助文档。 30 | 31 | 32 | 33 | #### Linux 系统查看 socket 函数帮助 34 | 35 | 如果是 Linux 系统,你可以通过 man 手册去查看相应的函数签名和用法。举个例子,如果你要查看 connect 函数的用法,只需要在 Linux shell 终端输入 **man connect** 即可。 36 | 37 | ``` 38 | [root@localhost ~]# man connect 39 | CONNECT(2) Linux Programmer's Manual CONNECT(2) 40 | 41 | NAME 42 | connect - initiate a connection on a socket 43 | 44 | SYNOPSIS 45 | #include /* See NOTES */ 46 | #include 47 | 48 | int connect(int sockfd, const struct sockaddr *addr, 49 | socklen_t addrlen); 50 | 51 | DESCRIPTION 52 | The connect() system call connects the socket referred to by the file descriptor sockfd to the address specified by addr. The addrlen argument specifies 53 | the size of addr. The format of the address in addr is determined by the address space of the socket sockfd; see socket(2) for further details. 54 | 55 | If the socket sockfd is of type SOCK_DGRAM then addr is the address to which datagrams are sent by default, and the only address from which datagrams are 56 | received. If the socket is of type SOCK_STREAM or SOCK_SEQPACKET, this call attempts to make a connection to the socket that is bound to the address 57 | specified by addr. 58 | 59 | Generally, connection-based protocol sockets may successfully connect() only once; connectionless protocol sockets may use connect() multiple times to 60 | change their association. Connectionless sockets may dissolve the association by connecting to an address with the sa_family member of sockaddr set to 61 | AF_UNSPEC (supported on Linux since kernel 2.2). 62 | 63 | RETURN VALUE 64 | If the connection or binding succeeds, zero is returned. On error, -1 is returned, and errno is set appropriately. 65 | 66 | ERRORS 67 | The following are general socket errors only. There may be other domain-specific error codes. 68 | 69 | EACCES For UNIX domain sockets, which are identified by pathname: Write permission is denied on the socket file, or search permission is denied for one 70 | of the directories in the path prefix. (See also path_resolution(7).) 71 | 72 | EACCES, EPERM 73 | The user tried to connect to a broadcast address without having the socket broadcast flag enabled or the connection request failed because of a 74 | local firewall rule. 75 | 76 | EADDRINUSE 77 | Local address is already in use. 78 | ``` 79 | 80 | 81 | 82 | 如上面的代码片段所示,man手册对于一个函数的说明一般包括如下几部分: 83 | 84 | - 函数声明及相关数据结构所在的头文件,你实际编码时如果需要使用这个函数必须包含该头文件; 85 | - 函数的签名,即该函数的参数类型、个数和返回值; 86 | - 函数用法说明,并可能包括一些注意事项; 87 | - 函数返回值说明; 88 | - 调用函数出错可能得到的错误码值; 89 | - 一些相关函数在 man 手册的位置索引。(connect 没有这个部分) 90 | 91 | 如下图所示: 92 | 93 | ![1544686239939](http://www.hootina.org/github_easyserverdev/20181213153121.png) 94 | 95 | 96 | 97 | 需要注意的是,这个方法不仅可以查 socket 函数也可以查看 Linux 下其他通用函数(如 fread)甚至一个 shell 命令(如 sleep)。以 sleep 为例,如果你想查程序中 sleep 函数的用法,由于Linux 内置有一个叫 sleep 的 shell 命令,如果你在 shell 窗口直接输入 **man sleep**,显示出来的默认会是 sleep 命令而不是我们要的 sleep 函数的帮助信息。 98 | 99 | ![20181213154914.png](http://www.hootina.org/github_easyserverdev/20181213154914.png) 100 | 101 | 102 | 103 | 我们可以通过 **man man** 命令查看一下 man 手册组成部分: 104 | 105 | ``` 106 | [root@localhost ~]# man man 107 | ## 无关的部分,省略... 108 | The table below shows the section numbers of the manual followed by the types of pages they contain. 109 | 110 | 1 Executable programs or shell commands 111 | 2 System calls (functions provided by the kernel) 112 | 3 Library calls (functions within program libraries) 113 | 4 Special files (usually found in /dev) 114 | 5 File formats and conventions eg /etc/passwd 115 | 6 Games 116 | 7 Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7) 117 | 8 System administration commands (usually only for root) 118 | 9 Kernel routines [Non standard] 119 | 120 | A manual page consists of several sections. 121 | ``` 122 | 123 | 通过上面的代码片段,我们可以看出来,man 手册的内容总共有9部分组成,而 sleep 函数属于上面的 Section 3,所以我们输入 **man 3 sleep** 就可以查看 sleep 函数的帮助信息了: 124 | 125 | ``` 126 | [root@localhost ~]# man 3 sleep 127 | SLEEP(3) Linux Programmer's Manual SLEEP(3) 128 | 129 | NAME 130 | sleep - sleep for the specified number of seconds 131 | 132 | SYNOPSIS 133 | #include 134 | 135 | unsigned int sleep(unsigned int seconds); 136 | 137 | DESCRIPTION 138 | sleep() makes the calling thread sleep until seconds seconds have elapsed or a signal arrives which is not ignored. 139 | 140 | RETURN VALUE 141 | Zero if the requested time has elapsed, or the number of seconds left to sleep, if the call was interrupted by a signal handler. 142 | 143 | CONFORMING TO 144 | POSIX.1-2001. 145 | 146 | BUGS 147 | sleep() may be implemented using SIGALRM; mixing calls to alarm(2) and sleep() is a bad idea. 148 | 149 | Using longjmp(3) from a signal handler or modifying the handling of SIGALRM while sleeping will cause undefined results. 150 | 151 | SEE ALSO 152 | alarm(2), nanosleep(2), signal(2), signal(7) 153 | 154 | COLOPHON 155 | This page is part of release 3.53 of the Linux man-pages project. A description of the project, and information about reporting bugs, can be found at 156 | http://www.kernel.org/doc/man-pages/. 157 | ``` 158 | 159 | 160 | 161 | #### Windows 上查看 socket 函数帮助 162 | 163 | Windows 也有类似 man 手册的帮助文档,早些年 Visual Studio 会自带一套离线的 MSDN 文档库,其优点就是不需要电脑联网,缺点是占磁盘空间比较大,内容陈旧。在手机网络都如此普及的今天,笔者还是建议使用在线版本的 MSDN。查看 Windows API 的帮助链接是:https://docs.microsoft.com/en-us/windows/desktop/,在页面的搜索框中输入你想要搜索的 API 函数即可。 164 | 165 | > 需要注意的是,建议读者在页面的底部将页面语言设置成English,这样搜索出来的内容会更准确更丰富。(本章节作者写于2018年12月13日,当读者实际看到本书内容时,可能这个页面已经改版了,请读者根据实际情况操作。)如下图所示: 166 | 167 | ![20181213170610.png](http://www.hootina.org/github_easyserverdev/20181213170610.png) 168 | 169 | 170 | 171 | 我们还是以 connect 函数为例,在上述页面的搜索框中输入 **socket connect** ,然后回车,得到一组搜索结果,我们选择我们需要的页面,打开链接:https://docs.microsoft.com/en-us/windows/desktop/api/winsock2/nf-winsock2-connect。与简陋的 man 手册相比,MSDN 关于connect 函数的说明就比较详细了,大体也分为以下几部分: 172 | 173 | 1. **Syntax**, 即函数签名,函数的参数类型、个数和返回值; 174 | 2. **Parameters**,参数的用法详细说明; 175 | 3. **Return Value**, 函数的返回值说明,在返回值部分,还有如果函数调用失败详细的错误码说明信息; 176 | 4. **Remarks**,这部分就是该函数的详细用法说明,某些函数还会给出示例代码; 177 | 5. **Requirements**,这部分指的是要使用这个函数,操作系统的版本要求,代码需要引入的头文件和库文件(如果有的话)。 178 | 6. **See Also**, 这部分一般是一些相关函数和知识点的链接信息。 179 | 180 | 181 | 182 | 需要注意的是,在 MSDN 上阅读相关 API 的帮助信息时,你要辩证性地对待其提供的信息,因为很多函数的实际工作原理和行为并不一定如 MSDN 介绍的那样。所以在有些 API 帮助下面会有一些读者的评论信息,这些评论信息或对文档内容做一些补充或纠错,或给出一些代码示例。建议读者实际查阅时,留意一下这部分信息,或许能得到一些很有用的帮助。 183 | 184 | 185 | 186 | 本书不会逐一介绍每个 socket 函数的基本用法,因为已经有大量的网络编程书籍介绍过了,这里就不再赘述。当然,这并不是说明它们不重要,相反**读者应该认真地把每一个函数的用法都学会**。但是关于这些函数的一些重要使用事项和原理我们还是要重点介绍一下。让我们开始吧。 187 | 188 | 189 | 190 | ------ 191 | 192 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 193 | 194 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 195 | 196 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) 197 | 198 | -------------------------------------------------------------------------------- /网络通信基础重难点解析 07 :非阻塞模式下 send 和 recv 函数的返回值总结.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 07 :非阻塞模式下 send 和 recv 函数的返回值总结 2 | 3 | 4 | 5 | #### 非阻塞模式下 send 和 recv 函数的返回值总结 6 | 7 | 我们来根据前面的讨论来总结一下 **send** 和 **recv** 函数的各种返回值意义: 8 | 9 | | 返回值 n | 返回值含义 | 10 | | :-----------: | :----------------------------------------------------------: | 11 | | 大于 0 | 成功发送 n 个字节 | 12 | | 0 | 对端关闭连接 | 13 | | 小于 0( -1) | 出错或者被信号中断或者对端 TCP 窗口太小数据发不出去(send)或者当前网卡缓冲区已无数据可收(recv) | 14 | 15 | 我们来逐一介绍下这三种情况: 16 | 17 | - **返回值大于 0** 18 | 19 | 对于 **send** 和 **recv** 函数返回值大于 **0**,表示发送或接收多少字节,需要注意的是,在这种情形下,我们一定要判断下 send 函数的返回值是不是我们期望发送的缓冲区长度,而不是简单判断其返回值大于 0。举个例子: 20 | 21 | ``` 22 | int n = send(socket, buf, buf_length, 0); 23 | if (n > 0) 24 | { 25 | printf("send data successfully\n"); 26 | } 27 | ``` 28 | 29 | 很多新手会写出上述代码,虽然返回值 n 大于 0,但是实际情形下,由于对端的 TCP 窗口可能因为缺少一部分字节就满了,所以返回值 n 的值可能在 (0, buf_length] 之间,当 0 < n < buf_length 时,虽然此时 send 函数是调用成功了,但是业务上并不算正确,因为有部分数据并没发出去。你可能在一次测试中测不出 n 不等于 buf_length 的情况,但是不代表实际中不存在。所以,建议要么认为返回值 n 等于 buf_length 才认为正确,要么在一个循环中调用 send 函数,如果数据一次性发不完,记录偏移量,下一次从偏移量处接着发,直到全部发送完为止。 30 | 31 | ``` 32 | //推荐的方式一 33 | int n = send(socket, buf, buf_length, 0); 34 | if (n == buf_length) 35 | { 36 | printf("send data successfully\n"); 37 | } 38 | ``` 39 | 40 | 41 | 42 | //推荐的方式二:在一个循环里面根据偏移量发送数据 43 | bool SendData(const char* buf, int buf_length) 44 | { 45 | //已发送的字节数目 46 | int sent_bytes = 0; 47 | int ret = 0; 48 | while (true) 49 | { 50 | ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0); 51 | if (ret == -1) 52 | { 53 | if (errno == EWOULDBLOCK) 54 | { 55 | //严谨的做法,这里如果发不出去,应该缓存尚未发出去的数据,后面介绍 56 | break; 57 | } 58 | else if (errno == EINTR) 59 | continue; 60 | else 61 | return false; 62 | } 63 | else if (ret == 0) 64 | { 65 | return false; 66 | } 67 | 68 | sent_bytes += ret; 69 | if (sent_bytes == buf_length) 70 | break; 71 | 72 | //稍稍降低 CPU 的使用率 73 | usleep(1); 74 | } 75 | 76 | return true; 77 | } 78 | 79 | 80 | ​ 81 | 82 | - **返回值等于 0** 83 | 84 | 通常情况下,如果 **send** 或者 **recv** 函数返回 **0**,我们就认为对端关闭了连接,我们这端也关闭连接即可,这是实际开发时最常见的处理逻辑。 85 | 86 | 但是,现在还有一种情形就是,假设调用 **send** 函数传递的数据长度就是 0 呢?**send** 函数会是什么行为?对端会 **recv** 到一个 0 字节的数据吗?需要强调的是,**在实际开发中,你不应该让你的程序有任何机会去 send 0 字节的数据,这是一种不好的做法。** 这里仅仅用于实验性讨论,我们来通过一个例子,来看下 **send** 一个长度为 **0** 的数据,**send** 函数的返回值是什么?对端会 **recv** 到 **0** 字节的数据吗? 87 | 88 | **server** 端代码: 89 | 90 | ``` 91 | /** 92 | * 验证recv函数接受0字节的行为,server端,server_recv_zero_bytes.cpp 93 | * zhangyl 2018.12.17 94 | */ 95 | #include 96 | #include 97 | #include 98 | #include 99 | #include 100 | #include 101 | #include 102 | 103 | int main(int argc, char* argv[]) 104 | { 105 | //1.创建一个侦听socket 106 | int listenfd = socket(AF_INET, SOCK_STREAM, 0); 107 | if (listenfd == -1) 108 | { 109 | std::cout << "create listen socket error." << std::endl; 110 | return -1; 111 | } 112 | 113 | //2.初始化服务器地址 114 | struct sockaddr_in bindaddr; 115 | bindaddr.sin_family = AF_INET; 116 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 117 | bindaddr.sin_port = htons(3000); 118 | if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 119 | { 120 | std::cout << "bind listen socket error." << std::endl; 121 | close(listenfd); 122 | return -1; 123 | } 124 | 125 | //3.启动侦听 126 | if (listen(listenfd, SOMAXCONN) == -1) 127 | { 128 | std::cout << "listen error." << std::endl; 129 | close(listenfd); 130 | return -1; 131 | } 132 | 133 | int clientfd; 134 | 135 | struct sockaddr_in clientaddr; 136 | socklen_t clientaddrlen = sizeof(clientaddr); 137 | //4. 接受客户端连接 138 | clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); 139 | if (clientfd != -1) 140 | { 141 | while (true) 142 | { 143 | char recvBuf[32] = {0}; 144 | //5. 从客户端接受数据,客户端没有数据来的时候会在 recv 函数处阻塞 145 | int ret = recv(clientfd, recvBuf, 32, 0); 146 | if (ret > 0) 147 | { 148 | std::cout << "recv data from client, data: " << recvBuf << std::endl; 149 | } 150 | else if (ret == 0) 151 | { 152 | std::cout << "recv 0 byte data." << std::endl; 153 | continue; 154 | } 155 | else 156 | { 157 | //出错 158 | std::cout << "recv data error." << std::endl; 159 | break; 160 | } 161 | } 162 | } 163 | 164 | 165 | //关闭客户端socket 166 | close(clientfd); 167 | //7.关闭侦听socket 168 | close(listenfd); 169 | 170 | return 0; 171 | } 172 | ``` 173 | 174 | 上述代码侦听端口号是 **3000**,代码 **55** 行调用了 **recv** 函数,如果客户端一直没有数据,程序会阻塞在这里。 175 | 176 | **client** 端代码: 177 | 178 | ``` 179 | /** 180 | * 验证非阻塞模式下send函数发送0字节的行为,client端,nonblocking_client_send_zero_bytes.cpp 181 | * zhangyl 2018.12.17 182 | */ 183 | #include 184 | #include 185 | #include 186 | #include 187 | #include 188 | #include 189 | #include 190 | #include 191 | #include 192 | 193 | #define SERVER_ADDRESS "127.0.0.1" 194 | #define SERVER_PORT 3000 195 | #define SEND_DATA "" 196 | 197 | int main(int argc, char* argv[]) 198 | { 199 | //1.创建一个socket 200 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 201 | if (clientfd == -1) 202 | { 203 | std::cout << "create client socket error." << std::endl; 204 | return -1; 205 | } 206 | 207 | //2.连接服务器 208 | struct sockaddr_in serveraddr; 209 | serveraddr.sin_family = AF_INET; 210 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 211 | serveraddr.sin_port = htons(SERVER_PORT); 212 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 213 | { 214 | std::cout << "connect socket error." << std::endl; 215 | close(clientfd); 216 | return -1; 217 | } 218 | 219 | //连接成功以后,我们再将 clientfd 设置成非阻塞模式, 220 | //不能在创建时就设置,这样会影响到 connect 函数的行为 221 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 222 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 223 | if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 224 | { 225 | close(clientfd); 226 | std::cout << "set socket to nonblock error." << std::endl; 227 | return -1; 228 | } 229 | 230 | //3. 不断向服务器发送数据,或者出错退出 231 | int count = 0; 232 | while (true) 233 | { 234 | //发送 0 字节的数据 235 | int ret = send(clientfd, SEND_DATA, 0, 0); 236 | if (ret == -1) 237 | { 238 | //非阻塞模式下send函数由于TCP窗口太小发不出去数据,错误码是EWOULDBLOCK 239 | if (errno == EWOULDBLOCK) 240 | { 241 | std::cout << "send data error as TCP Window size is too small." << std::endl; 242 | continue; 243 | } 244 | else if (errno == EINTR) 245 | { 246 | //如果被信号中断,我们继续重试 247 | std::cout << "sending data interrupted by signal." << std::endl; 248 | continue; 249 | } 250 | else 251 | { 252 | std::cout << "send data error." << std::endl; 253 | break; 254 | } 255 | } 256 | else if (ret == 0) 257 | { 258 | //对端关闭了连接,我们也关闭 259 | std::cout << "send 0 byte data." << std::endl; 260 | } 261 | else 262 | { 263 | count ++; 264 | std::cout << "send data successfully, count = " << count << std::endl; 265 | } 266 | 267 | //每三秒发一次 268 | sleep(3); 269 | } 270 | 271 | //5. 关闭socket 272 | close(clientfd); 273 | 274 | return 0; 275 | } 276 | ``` 277 | 278 | **client** 端连接服务器成功以后,每隔 3 秒调用 **send** 一次发送一个 0 字节的数据。除了先启动 **server** 以外,我们使用 tcpdump 抓一下经过端口 **3000** 上的数据包,使用如下命令: 279 | 280 | ``` 281 | tcpdump -i any 'tcp port 3000' 282 | ``` 283 | 284 | 然后启动 **client** ,我们看下结果: 285 | 286 | ![](http://www.hootina.org/github_easyserverdev/20190313180033.png) 287 | 288 | 客户端确实是每隔 3 秒 **send** 一次数据。此时我们使用 **lsof -i -Pn** 命令查看连接状态,也是正常的: 289 | 290 | ![](http://www.hootina.org/github_easyserverdev/20190313180454.png) 291 | 292 | 然后,tcpdump 抓包结果输出中,除了连接时的三次握手数据包,再也无其他数据包,也就是说,**send** 函数发送 **0** 字节数据,**client** 的协议栈并不会把这些数据发出去。 293 | 294 | ``` 295 | [root@localhost ~]# tcpdump -i any 'tcp port 3000' 296 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 297 | listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes 298 | 17:37:03.028449 IP localhost.48820 > localhost.hbci: Flags [S], seq 1632283330, win 43690, options [mss 65495,sackOK,TS val 201295556 ecr 0,nop,wscale 7], length 0 299 | 17:37:03.028479 IP localhost.hbci > localhost.48820: Flags [S.], seq 3669336158, ack 1632283331, win 43690, options [mss 65495,sackOK,TS val 201295556 ecr 201295556,nop,wscale 7], length 0 300 | 17:37:03.028488 IP localhost.48820 > localhost.hbci: Flags [.], ack 1, win 342, options [nop,nop,TS val 201295556 ecr 201295556], length 0 301 | 302 | ``` 303 | 304 | 因此,**server** 端也会一直没有输出,如果你用的是 gdb 启动 **server**,此时中断下来会发现,**server** 端由于没有数据会一直阻塞在 **recv** 函数调用处(**55** 行)。 305 | 306 | ![](http://www.hootina.org/github_easyserverdev/20190313180921.png) 307 | 308 | 309 | 310 | 上述示例再次验证了,**send** 一个 0 字节的数据没有任何意思,希望读者在实际开发时,避免写出这样的代码。 311 | 312 | 313 | 314 | 315 | 316 | ------ 317 | 318 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 319 | 320 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 321 | 322 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /网络通信基础重难点解析 16 :域名解析 API 介绍.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 16 :域名解析 API 介绍 2 | 3 | 为了便于记忆,有时候我们需要我们的程序可以使用域名和端口号去连接服务,这种情况下,我们需要使用 socket API **gethostbyname** 函数先把域名转换成 ip 地址,再使用 connect 函数连接。在 Linux 系统上, **gethostbyname** 函数的签名如下: 4 | 5 | ``` 6 | #include 7 | 8 | struct hostent* gethostbyname(const char* name); 9 | ``` 10 | 11 | 域名转换成 ip 时,转换结果存在一个 **hostent** 结构体中。转换成功后的 ip 地址存放在 **hostent** 最后一个字段中,**hostent** 结构体类型定义如下: 12 | 13 | ``` 14 | struct hostent 15 | { 16 | char* h_name; /* official name of host */ 17 | char** h_aliases; /* alias list */ 18 | int h_addrtype; /* host address type */ 19 | int h_length; /* length of address */ 20 | char** h_addr_list; /* list of addresses */ 21 | } 22 | 23 | #define h_addr h_addr_list[0] /* for backward compatibility */ 24 | ``` 25 | 26 | - 字段 **h_name**: 地址的正式名称; 27 | - 字段 **h_aliases**: 地址的预备名称指针; 28 | - 字段 **h_addrtype**: 地址类型,通常是AF_INET; 29 | - 字段 **h_length**: 地址的长度,以字节数目为计量单位; 30 | - 字段 **h_addr_list**:主机网络地址指针,网络字节顺序。 其中,**h_addr** 是字段 **h_addr_list** 中的第一地址。 31 | 32 | **注意**:虽然 **h_addr_list[0]** 看起来是一个 **char*** 类型,但实际上是一个 **uint32_t**,这是 ip 地址的 32 bit 整数表示形式,如果需要转换成十进制点分法字符串再调用 inet_ntoa() 函数即可。 33 | 34 | 我们来看一段示例代码: 35 | 36 | ``` 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | //extern int h_errno; 45 | 46 | bool connect_to_server(const char* server, short port) 47 | { 48 | int hSocket = socket(AF_INET, SOCK_STREAM, 0); 49 | if (hSocket == -1) 50 | return false; 51 | 52 | struct sockaddr_in addrSrv = { 0 }; 53 | struct hostent* pHostent = NULL; 54 | //unsigned int addr = 0; 55 | 56 | //如果传入的参数 server 的值是 somesite.com 这种域名域名形式则 if 条件成立, 57 | //接着调用 gethostbyname 解析域名为 4 字节的 ip 地址(整型) 58 | if (addrSrv.sin_addr.s_addr = inet_addr(server) == INADDR_NONE) 59 | { 60 | pHostent = gethostbyname(server); 61 | if (pHostent == NULL) 62 | return false; 63 | 64 | //当使用 gethostbyname 解析域名时可能会得到多个 ip 地址,一般最常用的使用第一个 ip 地址 65 | addrSrv.sin_addr.s_addr = *((unsigned long*)pHostent->h_addr_list[0]); 66 | } 67 | 68 | addrSrv.sin_family = AF_INET; 69 | addrSrv.sin_port = htons(port); 70 | int ret = connect(hSocket, (struct sockaddr*)&addrSrv, sizeof(addrSrv)); 71 | if (ret == -1) 72 | return false; 73 | 74 | return true; 75 | } 76 | 77 | int main() 78 | { 79 | if (connect_to_server("baidu.com", 80)) 80 | printf("connect successfully.\n"); 81 | else 82 | printf("connect error.\n"); 83 | 84 | return 0; 85 | } 86 | ``` 87 | 88 | 上述 **connect_to_server** 函数既可以支持直接传入域名,也可以传入 ip 地址: 89 | 90 | ``` 91 | connect_to_server("127.0.0.1", 8888); 92 | connect_to_server("localhost", 8888); 93 | 94 | connect_to_server("61.135.169.125", 80); 95 | connect_to_server("baidu.com", 80); 96 | ``` 97 | 98 | 实际在使用 **gethostbyname** 函数时需要注意以下: 99 | 100 | - **gethostbyname** 函数是不可重入函数,在 Linux 下建议使用 **gethostbyname_r** 函数替代; 101 | 102 | - **gethostbyname** 在解析域名时,会阻塞当前执行线程的,直到得到返回结果; 103 | 104 | - 在使用 **gethostbyname** 函数出错时,你不能使用 errno 获取错误码信息(因此也不能使用 **perror()** 函数打印错误信息),你应该使用 **h_errno** 错误码(也可以调用 **herror()** 打印错误信息),**herror()** 函数签名如下: 105 | 106 | ``` 107 | void herror(const char *s); 108 | ``` 109 | 110 | 111 | 112 | 在新的 Linux 系统中,gethostbyname 和 gethostbyaddr 一样,已经被标记为废弃的,你应该使用新的函数 **getaddrinfo** 去替代它们,**getaddrinfo** 签名如下: 113 | 114 | ``` 115 | #include 116 | #include 117 | #include 118 | 119 | int getaddrinfo(const char* node, 120 | const char* service, 121 | const struct addrinfo* hints, 122 | struct addrinfo** res); 123 | ``` 124 | 125 | **getaddrinfo** 函数调用成功返回 0,失败返回非 0 值,调用成功后结果存储在参数 **res** 中。**addrinfo** 结构体定义如下: 126 | 127 | ``` 128 | struct addrinfo 129 | { 130 | int ai_flags; 131 | int ai_family; 132 | int ai_socktype; 133 | int ai_protocol; 134 | socklen_t ai_addrlen; 135 | struct sockaddr* ai_addr; 136 | char* ai_canonname; 137 | struct addrinfo* ai_next; 138 | }; 139 | ``` 140 | 141 | 如果你不再需要 **res** 这个变量,记得使用 **freeaddrinfo** 函数将其指向的资源释放: 142 | 143 | ``` 144 | void freeaddrinfo(struct addrinfo* res); 145 | ``` 146 | 147 | **getaddrinfo** 使用示例如下: 148 | 149 | ``` 150 | struct addrinfo hints = {0}; 151 | hints.ai_flags = AI_CANONNAME; 152 | hints.ai_family = family; 153 | hints.ai_socktype = socktype; 154 | 155 | struct addrinfo* res; 156 | int n = getaddrinfo(host, service, &hints, &res); 157 | if(n == 0) 158 | { 159 | //调用成功,使用 res 160 | 161 | //释放 res 资源 162 | freeaddr(res); 163 | } 164 | ``` 165 | 166 | 167 | 168 | **getaddrinfo** 函数不仅支持 ipv4,同时也支持 ipv6 的解析,redis-server 的源码中就使用了这个函数去解析 ipv4 和 ipv6 的地址(位于 **net.c** 文件中): 169 | 170 | ``` 171 | static int _redisContextConnectTcp(redisContext *c, const char *addr, int port, 172 | const struct timeval *timeout, 173 | const char *source_addr) { 174 | int s, rv, n; 175 | char _port[6]; /* strlen("65535"); */ 176 | struct addrinfo hints, *servinfo, *bservinfo, *p, *b; 177 | int blocking = (c->flags & REDIS_BLOCK); 178 | int reuseaddr = (c->flags & REDIS_REUSEADDR); 179 | int reuses = 0; 180 | long timeout_msec = -1; 181 | 182 | servinfo = NULL; 183 | c->connection_type = REDIS_CONN_TCP; 184 | c->tcp.port = port; 185 | 186 | /* We need to take possession of the passed parameters 187 | * to make them reusable for a reconnect. 188 | * We also carefully check we don't free data we already own, 189 | * as in the case of the reconnect method. 190 | * 191 | * This is a bit ugly, but atleast it works and doesn't leak memory. 192 | **/ 193 | if (c->tcp.host != addr) { 194 | if (c->tcp.host) 195 | free(c->tcp.host); 196 | 197 | c->tcp.host = strdup(addr); 198 | } 199 | 200 | if (timeout) { 201 | if (c->timeout != timeout) { 202 | if (c->timeout == NULL) 203 | c->timeout = malloc(sizeof(struct timeval)); 204 | 205 | memcpy(c->timeout, timeout, sizeof(struct timeval)); 206 | } 207 | } else { 208 | if (c->timeout) 209 | free(c->timeout); 210 | c->timeout = NULL; 211 | } 212 | 213 | if (redisContextTimeoutMsec(c, &timeout_msec) != REDIS_OK) { 214 | __redisSetError(c, REDIS_ERR_IO, "Invalid timeout specified"); 215 | goto error; 216 | } 217 | 218 | if (source_addr == NULL) { 219 | free(c->tcp.source_addr); 220 | c->tcp.source_addr = NULL; 221 | } else if (c->tcp.source_addr != source_addr) { 222 | free(c->tcp.source_addr); 223 | c->tcp.source_addr = strdup(source_addr); 224 | } 225 | 226 | snprintf(_port, 6, "%d", port); 227 | memset(&hints,0,sizeof(hints)); 228 | hints.ai_family = AF_INET; 229 | hints.ai_socktype = SOCK_STREAM; 230 | 231 | /* Try with IPv6 if no IPv4 address was found. We do it in this order since 232 | * in a Redis client you can't afford to test if you have IPv6 connectivity 233 | * as this would add latency to every connect. Otherwise a more sensible 234 | * route could be: Use IPv6 if both addresses are available and there is IPv6 235 | * connectivity. */ 236 | if ((rv = getaddrinfo(c->tcp.host,_port,&hints,&servinfo)) != 0) { 237 | hints.ai_family = AF_INET6; 238 | if ((rv = getaddrinfo(addr,_port,&hints,&servinfo)) != 0) { 239 | __redisSetError(c,REDIS_ERR_OTHER,gai_strerror(rv)); 240 | return REDIS_ERR; 241 | } 242 | } 243 | for (p = servinfo; p != NULL; p = p->ai_next) { 244 | addrretry: 245 | if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1) 246 | continue; 247 | 248 | c->fd = s; 249 | if (redisSetBlocking(c,0) != REDIS_OK) 250 | goto error; 251 | if (c->tcp.source_addr) { 252 | int bound = 0; 253 | /* Using getaddrinfo saves us from self-determining IPv4 vs IPv6 */ 254 | if ((rv = getaddrinfo(c->tcp.source_addr, NULL, &hints, &bservinfo)) != 0) { 255 | char buf[128]; 256 | snprintf(buf,sizeof(buf),"Can't get addr: %s",gai_strerror(rv)); 257 | __redisSetError(c,REDIS_ERR_OTHER,buf); 258 | goto error; 259 | } 260 | 261 | if (reuseaddr) { 262 | n = 1; 263 | if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*) &n, 264 | sizeof(n)) < 0) { 265 | goto error; 266 | } 267 | } 268 | 269 | for (b = bservinfo; b != NULL; b = b->ai_next) { 270 | if (bind(s,b->ai_addr,b->ai_addrlen) != -1) { 271 | bound = 1; 272 | break; 273 | } 274 | } 275 | freeaddrinfo(bservinfo); 276 | if (!bound) { 277 | char buf[128]; 278 | snprintf(buf,sizeof(buf),"Can't bind socket: %s",strerror(errno)); 279 | __redisSetError(c,REDIS_ERR_OTHER,buf); 280 | goto error; 281 | } 282 | } 283 | if (connect(s,p->ai_addr,p->ai_addrlen) == -1) { 284 | if (errno == EHOSTUNREACH) { 285 | redisContextCloseFd(c); 286 | continue; 287 | } else if (errno == EINPROGRESS && !blocking) { 288 | /* This is ok. */ 289 | } else if (errno == EADDRNOTAVAIL && reuseaddr) { 290 | if (++reuses >= REDIS_CONNECT_RETRIES) { 291 | goto error; 292 | } else { 293 | redisContextCloseFd(c); 294 | goto addrretry; 295 | } 296 | } else { 297 | if (redisContextWaitReady(c,timeout_msec) != REDIS_OK) 298 | goto error; 299 | } 300 | } 301 | if (blocking && redisSetBlocking(c,1) != REDIS_OK) 302 | goto error; 303 | if (redisSetTcpNoDelay(c) != REDIS_OK) 304 | goto error; 305 | 306 | c->flags |= REDIS_CONNECTED; 307 | rv = REDIS_OK; 308 | goto end; 309 | } 310 | if (p == NULL) { 311 | char buf[128]; 312 | snprintf(buf,sizeof(buf),"Can't create socket: %s",strerror(errno)); 313 | __redisSetError(c,REDIS_ERR_OTHER,buf); 314 | goto error; 315 | } 316 | 317 | error: 318 | rv = REDIS_ERR; 319 | end: 320 | freeaddrinfo(servinfo); 321 | return rv; // Need to return REDIS_OK if alright 322 | } 323 | ``` 324 | 325 | 326 | 327 | 328 | 329 | ------ 330 | 331 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 332 | 333 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 334 | 335 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /网络通信基础重难点解析 11 :Linux poll 函数用法.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 11 :Linux poll 函数用法 2 | 3 | 4 | 5 | ### Linux poll 函数用法 6 | 7 | **poll** 函数用于检测一组文件描述符(**F**ile **D**escriptor, **fd**)上的可读可写和出错事件,其函数签名如下: 8 | 9 | ``` 10 | #include 11 | 12 | int poll(struct pollfd* fds, nfds_t nfds, int timeout); 13 | ``` 14 | 15 | **参数解释:** 16 | 17 | - **fds**:指向一个结构体数组的首个元素的指针,每个数组元素都是一个 **struct pollfd** 结构,用于指定检测某个给定的 fd 的条件; 18 | 19 | - **nfds**:参数 **fds** 结构体数组的长度,**nfds_t** 本质上是 **unsigned long int**,其定义如下: 20 | 21 | ``` 22 | typedef unsigned long int nfds_t; 23 | ``` 24 | 25 | - **timeout**:表示 poll 函数的超时时间,单位为毫秒。 26 | 27 | 28 | 29 | **struct pollfd** 结构体定义如下: 30 | 31 | ``` 32 | struct pollfd { 33 | int fd; /* 待检测事件的 fd */ 34 | short events; /* 关心的事件组合 */ 35 | short revents; /* 检测后的得到的事件类型 */ 36 | }; 37 | ``` 38 | 39 | **struct pollfd**的 **events** 字段是由开发者来设置,告诉内核我们关注什么事件,而 **revents** 字段是 **poll** 函数返回时内核设置的,用以说明该 fd 发生了什么事件。**events** 和 **revents** 一般有如下取值: 40 | 41 | | 事件宏 | 事件描述 | 是否可以作为输入(events) | 是否可以作为输出(revents) | 42 | | :--------: | :----------------------------------------------: | :------------------------: | :-------------------------: | 43 | | POLLIN | 数据可读(包括普通数据&优先数据) | 是 | 是 | 44 | | POLLOUT | 数据可写(普通数据&优先数据) | 是 | 是 | 45 | | POLLRDNORM | 等同于 POLLIN | 是 | 是 | 46 | | POLLRDBAND | 优先级带数据可读(一般用于 Linux 系统) | 是 | 是 | 47 | | POLLPRI | 高优先级数据可读,例如 TCP 带外数据 | 是 | 是 | 48 | | POLLWRNORM | 等同于 POLLOUT | 是 | 是 | 49 | | POLLWRBAND | 优先级带数据可写 | 是 | 是 | 50 | | POLLRDHUP | TCP连接被对端关闭,或者关闭了写操作,由 GNU 引入 | 是 | 是 | 51 | | POPPHUP | 挂起 | 否 | 是 | 52 | | POLLERR | 错误 | 否 | 是 | 53 | | POLLNVAL | 文件描述符没有打开 | 否 | 是 | 54 | 55 | 56 | 57 | **poll** 检测一组 fd 上的可读可写和出错事件的概念与前文介绍 **select** 的事件含义一样,这里就不再赘述。**poll** 与 **select** 相比具有如下优点: 58 | 59 | - **poll** 不要求开发者计算最大文件描述符加 1 的大小; 60 | - 相比于 **select**,**poll** 在处理大数目的文件描述符的时候速度更快; 61 | - **poll** 没有最大连接数的限制,原因是它是基于链表来存储的; 62 | - 在调用 **poll** 函数时,只需要对参数进行一次设置就好了。 63 | 64 | 我们来看一个具体的例子: 65 | 66 | ``` 67 | /** 68 | * 演示 poll 函数的用法,poll_server.cpp 69 | * zhangyl 2019.03.16 70 | */ 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | #include 78 | #include 79 | #include 80 | #include 81 | 82 | //无效fd标记 83 | #define INVALID_FD -1 84 | 85 | int main(int argc, char* argv[]) 86 | { 87 | //创建一个侦听socket 88 | int listenfd = socket(AF_INET, SOCK_STREAM, 0); 89 | if (listenfd == -1) 90 | { 91 | std::cout << "create listen socket error." << std::endl; 92 | return -1; 93 | } 94 | 95 | //将侦听socket设置为非阻塞的 96 | int oldSocketFlag = fcntl(listenfd, F_GETFL, 0); 97 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 98 | if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) 99 | { 100 | close(listenfd); 101 | std::cout << "set listenfd to nonblock error." << std::endl; 102 | return -1; 103 | } 104 | 105 | //复用地址和端口号 106 | int on = 1; 107 | setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)); 108 | setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on)); 109 | 110 | //初始化服务器地址 111 | struct sockaddr_in bindaddr; 112 | bindaddr.sin_family = AF_INET; 113 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 114 | bindaddr.sin_port = htons(3000); 115 | if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 116 | { 117 | std::cout << "bind listen socket error." << std::endl; 118 | close(listenfd); 119 | return -1; 120 | } 121 | 122 | //启动侦听 123 | if (listen(listenfd, SOMAXCONN) == -1) 124 | { 125 | std::cout << "listen error." << std::endl; 126 | close(listenfd); 127 | return -1; 128 | } 129 | 130 | std::vector fds; 131 | pollfd listen_fd_info; 132 | listen_fd_info.fd = listenfd; 133 | listen_fd_info.events = POLLIN; 134 | listen_fd_info.revents = 0; 135 | fds.push_back(listen_fd_info); 136 | 137 | //是否存在无效的fd标志 138 | bool exist_invalid_fd; 139 | int n; 140 | while (true) 141 | { 142 | exist_invalid_fd = false; 143 | n = poll(&fds[0], fds.size(), 1000); 144 | if (n < 0) 145 | { 146 | //被信号中断 147 | if (errno == EINTR) 148 | continue; 149 | 150 | //出错,退出 151 | break; 152 | } 153 | else if (n == 0) 154 | { 155 | //超时,继续 156 | continue; 157 | } 158 | 159 | for (size_t i = 0; i < fds.size(); ++i) 160 | { 161 | // 事件可读 162 | if (fds[i].revents & POLLIN) 163 | { 164 | if (fds[i].fd == listenfd) 165 | { 166 | //侦听socket,接受新连接 167 | struct sockaddr_in clientaddr; 168 | socklen_t clientaddrlen = sizeof(clientaddr); 169 | //接受客户端连接, 并加入到fds集合中 170 | int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); 171 | if (clientfd != -1) 172 | { 173 | //将客户端socket设置为非阻塞的 174 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 175 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 176 | if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 177 | { 178 | close(clientfd); 179 | std::cout << "set clientfd to nonblock error." << std::endl; 180 | } 181 | else 182 | { 183 | struct pollfd client_fd_info; 184 | client_fd_info.fd = clientfd; 185 | client_fd_info.events = POLLIN; 186 | client_fd_info.revents = 0; 187 | fds.push_back(client_fd_info); 188 | std::cout << "new client accepted, clientfd: " << clientfd << std::endl; 189 | } 190 | } 191 | } 192 | else 193 | { 194 | //普通clientfd,收取数据 195 | char buf[64] = { 0 }; 196 | int m = recv(fds[i].fd, buf, 64, 0); 197 | if (m <= 0) 198 | { 199 | if (errno != EINTR && errno != EWOULDBLOCK) 200 | { 201 | //出错或对端关闭了连接,关闭对应的clientfd,并设置无效标志位 202 | for (std::vector::iterator iter = fds.begin(); iter != fds.end(); ++ iter) 203 | { 204 | if (iter->fd == fds[i].fd) 205 | { 206 | std::cout << "client disconnected, clientfd: " << fds[i].fd << std::endl; 207 | close(fds[i].fd); 208 | iter->fd = INVALID_FD; 209 | exist_invalid_fd = true; 210 | break; 211 | } 212 | } 213 | } 214 | } 215 | else 216 | { 217 | std::cout << "recv from client: " << buf << ", clientfd: " << fds[i].fd << std::endl; 218 | } 219 | } 220 | } 221 | else if (fds[i].revents & POLLERR) 222 | { 223 | //TODO: 暂且不处理 224 | } 225 | 226 | }// end outer-for-loop 227 | 228 | if (exist_invalid_fd) 229 | { 230 | //统一清理无效的fd 231 | for (std::vector::iterator iter = fds.begin(); iter != fds.end(); ) 232 | { 233 | if (iter->fd == INVALID_FD) 234 | iter = fds.erase(iter); 235 | else 236 | ++iter; 237 | } 238 | } 239 | }// end while-loop 240 | 241 | 242 | //关闭所有socket 243 | for (std::vector::iterator iter = fds.begin(); iter != fds.end(); ++ iter) 244 | close(iter->fd); 245 | 246 | return 0; 247 | } 248 | ``` 249 | 250 | 编译上述程序生成 **poll_server** 并运行,然后使用 **nc** 命令模拟三个客户端并给 **poll_server** 发送消息,效果如下: 251 | 252 | ![](http://www.hootina.org/github_easyserverdev/20190316115558.png) 253 | 254 | 255 | 256 | > 由于 nc 命令是以 **\n** 作为结束标志的,所以 **poll_server** 收到客户端消息时显示时分两行的。 257 | 258 | 259 | 260 | 通过上面的示例代码,我们也能看出 **poll** 函数存在的一些缺点: 261 | 262 | - 在调用 **poll** 函数时,不管有没有有意义,大量的 fd 的数组被整体在用户态和内核地址空间之间复制; 263 | - 与 select 函数一样,poll 函数返回后,需要遍历 fd 集合来获取就绪的 fd,这样会使性能下降; 264 | - 同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。 265 | 266 | 267 | 268 | 与 **select** 函数实现非阻塞的 connect 原理一样,我们可以使用 poll 去实现,即通过 poll 检测 clientfd 在一定时间内是否可写,示例代码如下: 269 | 270 | ``` 271 | /** 272 | * Linux 下使用poll实现异步的connect,linux_nonblocking_connect_poll.cpp 273 | * zhangyl 2019.03.16 274 | */ 275 | #include 276 | #include 277 | #include 278 | #include 279 | #include 280 | #include 281 | #include 282 | #include 283 | #include 284 | #include 285 | 286 | #define SERVER_ADDRESS "127.0.0.1" 287 | #define SERVER_PORT 3000 288 | #define SEND_DATA "helloworld" 289 | 290 | int main(int argc, char* argv[]) 291 | { 292 | //1.创建一个socket 293 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 294 | if (clientfd == -1) 295 | { 296 | std::cout << "create client socket error." << std::endl; 297 | return -1; 298 | } 299 | 300 | //连接成功以后,我们再将 clientfd 设置成非阻塞模式, 301 | //不能在创建时就设置,这样会影响到 connect 函数的行为 302 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 303 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 304 | if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) 305 | { 306 | close(clientfd); 307 | std::cout << "set socket to nonblock error." << std::endl; 308 | return -1; 309 | } 310 | 311 | //2.连接服务器 312 | struct sockaddr_in serveraddr; 313 | serveraddr.sin_family = AF_INET; 314 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 315 | serveraddr.sin_port = htons(SERVER_PORT); 316 | for (;;) 317 | { 318 | int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); 319 | if (ret == 0) 320 | { 321 | std::cout << "connect to server successfully." << std::endl; 322 | close(clientfd); 323 | return 0; 324 | } 325 | else if (ret == -1) 326 | { 327 | if (errno == EINTR) 328 | { 329 | //connect 动作被信号中断,重试connect 330 | std::cout << "connecting interruptted by signal, try again." << std::endl; 331 | continue; 332 | } else if (errno == EINPROGRESS) 333 | { 334 | //连接正在尝试中 335 | break; 336 | } else { 337 | //真的出错了, 338 | close(clientfd); 339 | return -1; 340 | } 341 | } 342 | } 343 | 344 | pollfd event; 345 | event.fd = clientfd; 346 | event.events = POLLOUT; 347 | int timeout = 3000; 348 | if (poll(&event, 1, timeout) != 1) 349 | { 350 | close(clientfd); 351 | std::cout << "[poll] connect to server error." << std::endl; 352 | return -1; 353 | } 354 | 355 | if (!(event.revents & POLLOUT)) 356 | { 357 | close(clientfd); 358 | std::cout << "[POLLOUT] connect to server error." << std::endl; 359 | return -1; 360 | } 361 | 362 | int err; 363 | socklen_t len = static_cast(sizeof err); 364 | if (::getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) 365 | return -1; 366 | 367 | if (err == 0) 368 | std::cout << "connect to server successfully." << std::endl; 369 | else 370 | std::cout << "connect to server error." << std::endl; 371 | 372 | //5. 关闭socket 373 | close(clientfd); 374 | 375 | return 0; 376 | } 377 | ``` 378 | 379 | 运行效果与前面的 **select** 实现这个非阻塞的 connect 一样,这里就不再给出运行效果图了。 380 | 381 | 382 | 383 | 384 | 385 | ------ 386 | 387 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 388 | 389 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 390 | 391 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /网络通信基础重难点解析 13 :Windows WSAEventSelect 网络通信模型.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 13 :Windows WSAEventSelect 网络通信模型 2 | 3 | 4 | 5 | ### Windows WSAEventSelect 网络通信模型 6 | 7 | **WSAEventSelect** 网络通信模型是 Windows 系统上常用的一种异步 socket 通信模型,下面来详细介绍下其用法。 8 | 9 | #### WSAEventSelect 用于服务器端 10 | 11 | 我们先从服务器端来看这个模型,在 Windows 系统上正常的一个服务器端 socket 通信流程是先初始化套接字库,然后创建侦听 socket,接着绑定 ip 地址和端口,再调用 **listen** 函数开启侦听。代码如下: 12 | 13 | ``` 14 | //1. 初始化套接字库 15 | WORD wVersionRequested; 16 | WSADATA wsaData; 17 | wVersionRequested = MAKEWORD(1, 1); 18 | int nError = WSAStartup(wVersionRequested, &wsaData); 19 | if (nError != 0) 20 | return -1; 21 | 22 | if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) 23 | { 24 | WSACleanup(); 25 | return -1; 26 | } 27 | 28 | //2. 创建用于监听的套接字 29 | SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0); 30 | SOCKADDR_IN addrSrv; 31 | addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); 32 | addrSrv.sin_family = AF_INET; 33 | addrSrv.sin_port = htons(6000); 34 | 35 | //3. 绑定套接字 36 | if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR) 37 | { 38 | closesocket(sockSrv); 39 | WSACleanup(); 40 | return -1; 41 | } 42 | 43 | 44 | //4. 将套接字设为监听模式,准备接受客户请求 45 | if (listen(sockSrv, SOMAXCONN) == SOCKET_ERROR) 46 | { 47 | closesocket(sockSrv); 48 | WSACleanup(); 49 | return -1; 50 | } 51 | ``` 52 | 53 | 54 | 55 | 正常的流程下接着是等待客户端连接,然后调用 **accept** 接受客户端连接。在这里,我们使用 **WSAEventSelect** 函数给侦听 socket 设置需要关注的事件。**WSAEventSelect** 的函数如下: 56 | 57 | ``` 58 | int WSAAPI WSAEventSelect( 59 | SOCKET s, 60 | WSAEVENT hEventObject, 61 | long lNetworkEvents 62 | ); 63 | ``` 64 | 65 | - 参数 **s** 是需要操作的 socket 句柄; 66 | 67 | - 参数 **hEventObject** 是需要与 socket 关联的内核事件对象,可以使用 **WSACreateEvent** 函数创建: 68 | 69 | ``` 70 | WSAEVENT WSAAPI WSACreateEvent(); 71 | ``` 72 | 73 | **WSAEVENT** 类型本质上就是使用 **CreateEvent** 创建的 Event 对象: 74 | 75 | ``` 76 | #define WSAEVENT HANDLE 77 | ``` 78 | 79 | - 参数 **lNetworkEvents** 是 socket 上需要关注的事件,常用的事件类型有: 80 | 81 | | 事件宏 | 事件含义 | 82 | | :--------: | :----------------------------: | 83 | | FD_READ | socket 可读 | 84 | | FD_WRITE | socket 可写 | 85 | | FD_ACCEPT | 侦听 socket 有新连接 | 86 | | FD_CONNECT | 普通 socket 连接服务器得到响应 | 87 | | FD_CLOSE | 连接关闭 | 88 | 89 | - **返回值**:**WSAEventSelect** 函数调用成功返回 0,调用失败返回 SOCKET_ERROR(-1)。 90 | 91 | 92 | 93 | 由于我们这里是侦听 socket,所以我们关注的事件是 **FD_ACCEPT**,代码如下: 94 | 95 | ``` 96 | WSAEVENT hListenEvent = WSACreateEvent(); 97 | if (WSAEventSelect(sockSrv, hListenEvent, FD_ACCEPT) == SOCKET_ERROR) 98 | { 99 | WSACloseEvent(hListenEvent); 100 | closesocket(sockSrv); 101 | WSACleanup(); 102 | return -1; 103 | } 104 | ``` 105 | 106 | 当 socket 上有我们关注的事件时,操作系统会让 **hListenEvent** 对象受信,所以接着我们使用 **WSAWaitForMultipleEvents** 函数去等待 **hListenEvent** 是否有信号,**WSAWaitForMultipleEvents** 签名如下: 107 | 108 | ``` 109 | DWORD WSAAPI WSAWaitForMultipleEvents( 110 | DWORD cEvents, 111 | const WSAEVENT* lphEvents, 112 | BOOL fWaitAll, 113 | DWORD dwTimeout, 114 | BOOL fAlertable 115 | ); 116 | ``` 117 | 118 | 这个函数的使用方法和 **WaitForMultipleObjects** 一模一样,我们在第三章介绍过了,这里不再介绍。 119 | 120 | 调用 **WSAWaitForMultipleEvents** 示例代码如下: 121 | 122 | ``` 123 | WSAEVENT hEvents[1]; 124 | hEvents[0] = hListenEvent; 125 | DWORD dwResult = WSAWaitForMultipleEvents(1, hEvents, FALSE, WSA_INFINITE, FALSE); 126 | 127 | DWORD dwIndex = dwResult - WSA_WAIT_EVENT_0; 128 | for (DWORD i = 0; i < dwIndex; ++i) 129 | { 130 | //通过dwIndex编号找到hEvents数组中的WSAEvent对象,进而找到对应的socket 131 | } 132 | ``` 133 | 134 | 通过 **dwIndex** 编号找到 **hEvents** 数组中的 **WSAEvent** 对象,进而找到对应的 socket,然后对这个 socket 调用 **WSAEnumNetworkEvents** 函数来获取该 socket 上的事件类型,**WSAEnumNetworkEvents** 函数签名如下: 135 | 136 | ``` 137 | int WSAAPI WSAEnumNetworkEvents( 138 | SOCKET s, 139 | WSAEVENT hEventObject, 140 | LPWSANETWORKEVENTS lpNetworkEvents 141 | ); 142 | ``` 143 | 144 | 参数 **lpNetworkEvents** 是一个输出参数,其类型是 **WSANETWORKEVENTS** 结构体指针,其定义如下: 145 | 146 | ``` 147 | typedef struct _WSANETWORKEVENTS { 148 | long lNetworkEvents; 149 | int iErrorCode[FD_MAX_EVENTS]; 150 | } WSANETWORKEVENTS, *LPWSANETWORKEVENTS; 151 | ``` 152 | 153 | 在调用 **WSAEnumNetworkEvents** 后我们就能通过 **lNetworkEvents** 类型得到对应的 socket 的事件类型,通过 **iErrorCode** 字段数组中的某一位确定该类型的事件是否有错误(**0** 值表示没有错误,**非 0** 值表示存在错误),与 **FD_XXX** 相对应,**iErrorCode** 每个下标都有确定的含义,下标值都被定义成了相应的宏,常见的有: 154 | 155 | ``` 156 | /* 157 | * WinSock 2 extension -- bit values and indices for FD_XXX network events 158 | */ 159 | #define FD_READ_BIT 0 160 | #define FD_READ (1 << FD_READ_BIT) 161 | 162 | #define FD_WRITE_BIT 1 163 | #define FD_WRITE (1 << FD_WRITE_BIT) 164 | 165 | #define FD_OOB_BIT 2 166 | #define FD_OOB (1 << FD_OOB_BIT) 167 | 168 | #define FD_ACCEPT_BIT 3 169 | #define FD_ACCEPT (1 << FD_ACCEPT_BIT) 170 | 171 | #define FD_CONNECT_BIT 4 172 | #define FD_CONNECT (1 << FD_CONNECT_BIT) 173 | 174 | #define FD_CLOSE_BIT 5 175 | #define FD_CLOSE (1 << FD_CLOSE_BIT) 176 | 177 | #define FD_QOS_BIT 6 178 | #define FD_QOS (1 << FD_QOS_BIT) 179 | 180 | #define FD_GROUP_QOS_BIT 7 181 | #define FD_GROUP_QOS (1 << FD_GROUP_QOS_BIT) 182 | 183 | #define FD_ROUTING_INTERFACE_CHANGE_BIT 8 184 | #define FD_ROUTING_INTERFACE_CHANGE (1 << FD_ROUTING_INTERFACE_CHANGE_BIT) 185 | 186 | #define FD_ADDRESS_LIST_CHANGE_BIT 9 187 | #define FD_ADDRESS_LIST_CHANGE (1 << FD_ADDRESS_LIST_CHANGE_BIT) 188 | 189 | #define FD_MAX_EVENTS 10 190 | #define FD_ALL_EVENTS ((1 << FD_MAX_EVENTS) - 1) 191 | ``` 192 | 193 | 194 | 195 | **WSAEnumNetworkEvents** 函数使用示例代码如下: 196 | 197 | ``` 198 | WSANETWORKEVENTS triggeredEvents; 199 | if (WSAEnumNetworkEvents(sockSrv, hEvents[dwIndex], &triggeredEvents) != SOCKET_ERROR) 200 | { 201 | if (triggeredEvents.lNetworkEvents & FD_ACCEPT) 202 | { 203 | // 0 值表示无错误 204 | if (triggeredEvents.iErrorCode[FD_ACCEPT_BIT] == 0) 205 | { 206 | //TODO:在这里可以调用accept函数处理接受连接事件。 207 | } 208 | } 209 | } 210 | ``` 211 | 212 | 上述代码第 **9** 行我们可以调用 **accept** 函数接受新连接,然后将新产生的 **clientsocket** 设置监听 FD_READ 和 FD_CLOSE 等事件。完整的代码如下所示: 213 | 214 | ``` 215 | /** 216 | * WSAEventSelect 模型演示 217 | * zhangyl 2019.03.16 218 | */ 219 | #include "stdafx.h" 220 | #include 221 | #include 222 | #include 223 | 224 | #pragma comment(lib, "ws2_32.lib") 225 | 226 | int main(int argc, _TCHAR* argv[]) 227 | { 228 | //1. 初始化套接字库 229 | WORD wVersionRequested; 230 | WSADATA wsaData; 231 | wVersionRequested = MAKEWORD(1, 1); 232 | int nError = WSAStartup(wVersionRequested, &wsaData); 233 | if (nError != 0) 234 | return -1; 235 | 236 | if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) 237 | { 238 | WSACleanup(); 239 | return -1; 240 | } 241 | 242 | //2. 创建用于监听的套接字 243 | SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0); 244 | SOCKADDR_IN addrSrv; 245 | addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); 246 | addrSrv.sin_family = AF_INET; 247 | addrSrv.sin_port = htons(6000); 248 | 249 | //3. 绑定套接字 250 | if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR) 251 | { 252 | closesocket(sockSrv); 253 | WSACleanup(); 254 | return -1; 255 | } 256 | 257 | //4. 将套接字设为监听模式,准备接受客户请求 258 | if (listen(sockSrv, SOMAXCONN) == SOCKET_ERROR) 259 | { 260 | closesocket(sockSrv); 261 | WSACleanup(); 262 | return -1; 263 | } 264 | 265 | WSAEVENT hListenEvent = WSACreateEvent(); 266 | if (WSAEventSelect(sockSrv, hListenEvent, FD_ACCEPT) == SOCKET_ERROR) 267 | { 268 | WSACloseEvent(hListenEvent); 269 | closesocket(sockSrv); 270 | WSACleanup(); 271 | return -1; 272 | } 273 | 274 | 275 | WSAEVENT* pEvents = new WSAEVENT[1]; 276 | pEvents[0] = hListenEvent; 277 | SOCKET* pSockets = new SOCKET[1]; 278 | pSockets[0] = sockSrv; 279 | DWORD dwCount = 1; 280 | bool bNeedToMove; 281 | 282 | while (true) 283 | { 284 | bNeedToMove = false; 285 | DWORD dwResult = WSAWaitForMultipleEvents(dwCount, pEvents, FALSE, WSA_INFINITE, FALSE); 286 | if (dwResult == WSA_WAIT_FAILED) 287 | continue; 288 | 289 | DWORD dwIndex = dwResult - WSA_WAIT_EVENT_0; 290 | for (DWORD i = 0; i <= dwIndex; ++i) 291 | { 292 | //通过dwIndex编号找到hEvents数组中的WSAEvent对象,进而找到对应的socket 293 | WSANETWORKEVENTS triggeredEvents; 294 | if (WSAEnumNetworkEvents(pSockets[i], pEvents[i], &triggeredEvents) == SOCKET_ERROR) 295 | continue; 296 | 297 | if (triggeredEvents.lNetworkEvents & FD_ACCEPT) 298 | { 299 | if (triggeredEvents.iErrorCode[FD_ACCEPT_BIT] != 0) 300 | continue; 301 | 302 | //调用accept函数处理接受连接事件; 303 | SOCKADDR_IN addrClient; 304 | int len = sizeof(SOCKADDR); 305 | //等待客户请求到来 306 | SOCKET hSockClient = accept(sockSrv, (SOCKADDR*)&addrClient, &len); 307 | if (hSockClient != SOCKET_ERROR) 308 | { 309 | //监听客户端socket的可读和关闭事件 310 | WSAEVENT hClientEvent = WSACreateEvent(); 311 | if (WSAEventSelect(hSockClient, hClientEvent, FD_READ | FD_CLOSE) == SOCKET_ERROR) 312 | { 313 | WSACloseEvent(hClientEvent); 314 | closesocket(hSockClient); 315 | continue; 316 | } 317 | 318 | WSAEVENT* pEvents2 = new WSAEVENT[dwCount + 1]; 319 | SOCKET* pSockets2 = new SOCKET[dwCount + 1]; 320 | memcpy(pEvents2, pEvents, dwCount * sizeof(WSAEVENT)); 321 | pEvents2[dwCount] = hClientEvent; 322 | memcpy(pSockets2, pSockets, dwCount * sizeof(SOCKET)); 323 | pSockets2[dwCount] = hSockClient; 324 | delete[] pEvents; 325 | delete[] pSockets; 326 | pEvents = pEvents2; 327 | pSockets = pSockets2; 328 | 329 | dwCount++; 330 | 331 | printf("a client connected, socket: %d, current: %d\n", (int)hSockClient, dwCount - 1); 332 | } 333 | } 334 | else if (triggeredEvents.lNetworkEvents & FD_READ) 335 | { 336 | if (triggeredEvents.iErrorCode[FD_READ_BIT] != 0) 337 | continue; 338 | 339 | char szBuf[64] = { 0 }; 340 | int nRet = recv(pSockets[i], szBuf, 64, 0); 341 | if (nRet > 0) 342 | { 343 | printf("recv data: %s, client: %d\n", szBuf, pSockets[i]); 344 | } 345 | } 346 | else if (triggeredEvents.lNetworkEvents & FD_CLOSE) 347 | { 348 | //此处不要判断 349 | //if (triggeredEvents.iErrorCode[FD_READ_BIT] != 0) 350 | // continue; 351 | 352 | printf("a client disconnected, socket: %d, current: %d\n", (int)pSockets[i], dwCount - 2); 353 | 354 | WSACloseEvent(pEvents[i]); 355 | closesocket(pSockets[i]); 356 | 357 | //标记为无效,循环结束后统一移除 358 | pSockets[i] = INVALID_SOCKET; 359 | 360 | bNeedToMove = true; 361 | } 362 | 363 | }// end for-loop 364 | 365 | if (bNeedToMove) 366 | { 367 | //移除无效的事件 368 | std::vector vValidSockets; 369 | std::vector vValidEvents; 370 | for (size_t i = 0; i < dwCount; ++i) 371 | { 372 | if (pSockets[i] != INVALID_SOCKET) 373 | { 374 | vValidSockets.push_back(pSockets[i]); 375 | vValidEvents.push_back(pEvents[i]); 376 | } 377 | } 378 | 379 | size_t validSize = vValidSockets.size(); 380 | if (validSize > 0) 381 | { 382 | WSAEVENT* pEvents2 = new WSAEVENT[validSize]; 383 | SOCKET* pSockets2 = new SOCKET[validSize]; 384 | memcpy(pEvents2, &vValidEvents[0], validSize * sizeof(WSAEVENT)); 385 | memcpy(pSockets2, &vValidSockets[0], validSize * sizeof(SOCKET)); 386 | delete[] pEvents; 387 | delete[] pSockets; 388 | pEvents = pEvents2; 389 | pSockets = pSockets2; 390 | 391 | dwCount = validSize; 392 | } 393 | } 394 | 395 | }// end while-loop 396 | 397 | closesocket(sockSrv); 398 | 399 | WSACleanup(); 400 | 401 | return 0; 402 | } 403 | ``` 404 | 405 | 在 Visual Studio 2013 中编译该程序并运行,然后使用 Linux nc 命令模拟几个客户端连接该程序,效果如下所示: 406 | 407 | ![](http://www.hootina.org/github_easyserverdev/20190316222936.png) 408 | 409 | 410 | 411 | 412 | 413 | ------ 414 | 415 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 416 | 417 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 418 | 419 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /网络通信基础重难点解析 03:bind 函数.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 03:bind 函数 2 | 3 | 4 | 5 | #### bind 函数如何选择绑定地址 6 | 7 | 上一节的服务器代码中演示了 bind 函数的使用方法,让我们再看一下相关的代码: 8 | 9 | ``` 10 | struct sockaddr_in bindaddr; 11 | bindaddr.sin_family = AF_INET; 12 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 13 | bindaddr.sin_port = htons(3000); 14 | if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 15 | { 16 | std::cout << "bind listen socket error." << std::endl; 17 | return -1; 18 | } 19 | ``` 20 | 21 | 其中 bind 的地址我们使用了一个宏叫 **INADDR_ANY** ,关于这个宏的解释如下: 22 | 23 | ``` 24 | If an application does not care what local address is assigned, specify the constant value INADDR_ANY for an IPv4 local address or the constant value in6addr_any for an IPv6 local address in the sa_data member of the name parameter. This allows the underlying service provider to use any appropriate network address, potentially simplifying application programming in the presence of multihomed hosts (that is, hosts that have more than one network interface and address). 25 | ``` 26 | 27 | 意译一下: 28 | 29 | ``` 30 | 如果应用程序不关心bind绑定的ip地址,可以使用INADDR_ANY(如果是IPv6,则对应in6addr_any),这样底层的(协议栈)服务会自动选择一个合适的ip地址,这样使在一个有多个网卡机器上选择ip地址问题变得简单。 31 | ``` 32 | 33 | 也就是说 **INADDR_ANY** 相当于地址 **0.0.0.0**。可能读者还是不太明白我想表达什么。这里我举个例子,假设我们在一台机器上开发一个服务器程序,使用 bind 函数时,我们有多个ip 地址可以选择。首先,这台机器对外访问的ip地址是 **120.55.94.78**,这台机器在当前局域网的地址是 **192.168.1.104**;同时这台机器有本地回环地址**127.0.0.1**。 34 | 35 | 如果你指向本机上可以访问,那么你 bind 函数中的地址就可以使用**127.0.0.1**; 如果你的服务只想被局域网内部机器访问,bind 函数的地址可以使用**192.168.1.104**;如果希望这个服务可以被公网访问,你就可以使用地址**0.0.0.0 ** 或 **INADDR_ANY**。 36 | 37 | 38 | 39 | #### bind 函数端口号问题 40 | 41 | 网络通信程序的基本逻辑是客户端连接服务器,即从客户端的**地址:端口**连接到服务器**地址:端口**上,以 4.2 小节中的示例程序为例,服务器端的端口号使用 3000,那客户端连接时的端口号是多少呢?TCP 通信双方中一般服务器端端口号是固定的,而客户端端口号是连接发起时由操作系统随机分配的(不会分配已经被占用的端口)。端口号是一个 C short 类型的值,其范围是0~65535,知道这点很重要,所以我们在编写压力测试程序时,由于端口数量的限制,在某台机器上网卡地址不变的情况下压力测试程序理论上最多只能发起六万五千多个连接。注意我说的是理论上,在实际情况下,由于当时的操作系统很多端口可能已经被占用,实际可以使用的端口比这个更少,例如,一般规定端口号在1024以下的端口是保留端口,不建议用户程序使用。而对于 Windows 系统,MSDN 甚至明确地说: 42 | 43 | ``` 44 | On Windows Vista and later, the dynamic client port range is a value between 49152 and 65535. This is a change from Windows Server 2003 and earlier where the dynamic client port range was a value between 1025 and 5000. 45 | Vista 及以后的Windows,可用的动态端口范围是49152~65535,而 Windows Server及更早的系统,可以的动态端口范围是1025~5000。(你可以通过修改注册表来改变这一设置,参考网址:https://docs.microsoft.com/en-us/windows/desktop/api/winsock/nf-winsock-bind) 46 | ``` 47 | 48 | 如果将 bind 函数中的端口号设置成0,那么操作系统会随机给程序分配一个可用的侦听端口,当然服务器程序一般不会这么做,因为服务器程序是要对外服务的,必须让客户端知道确切的ip地址和端口号。 49 | 50 | 很多人觉得只有服务器程序可以调用 bind 函数绑定一个端口号,其实不然,在一些特殊的应用中,我们需要客户端程序以指定的端口号去连接服务器,此时我们就可以在客户端程序中调用 bind 函数绑定一个具体的端口。 51 | 52 | 我们用代码来实际验证一下上路所说的,为了能看到连接状态,我们将客户端和服务器关闭socket的代码注释掉,这样连接会保持一段时间。 53 | 54 | - 情形一:客户端代码不绑定端口 55 | 56 | 修改后的服务器代码如下: 57 | 58 | ``` 59 | /** 60 | * TCP服务器通信基本流程 61 | * zhangyl 2018.12.13 62 | */ 63 | #include 64 | #include 65 | #include 66 | #include 67 | #include 68 | #include 69 | #include 70 | 71 | int main(int argc, char* argv[]) 72 | { 73 | //1.创建一个侦听socket 74 | int listenfd = socket(AF_INET, SOCK_STREAM, 0); 75 | if (listenfd == -1) 76 | { 77 | std::cout << "create listen socket error." << std::endl; 78 | return -1; 79 | } 80 | 81 | //2.初始化服务器地址 82 | struct sockaddr_in bindaddr; 83 | bindaddr.sin_family = AF_INET; 84 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 85 | bindaddr.sin_port = htons(3000); 86 | if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 87 | { 88 | std::cout << "bind listen socket error." << std::endl; 89 | return -1; 90 | } 91 | 92 | //3.启动侦听 93 | if (listen(listenfd, SOMAXCONN) == -1) 94 | { 95 | std::cout << "listen error." << std::endl; 96 | return -1; 97 | } 98 | 99 | //记录所有客户端连接的容器 100 | std::vector clientfds; 101 | while (true) 102 | { 103 | struct sockaddr_in clientaddr; 104 | socklen_t clientaddrlen = sizeof(clientaddr); 105 | //4. 接受客户端连接 106 | int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); 107 | if (clientfd != -1) 108 | { 109 | char recvBuf[32] = {0}; 110 | //5. 从客户端接受数据 111 | int ret = recv(clientfd, recvBuf, 32, 0); 112 | if (ret > 0) 113 | { 114 | std::cout << "recv data from client, data: " << recvBuf << std::endl; 115 | //6. 将收到的数据原封不动地发给客户端 116 | ret = send(clientfd, recvBuf, strlen(recvBuf), 0); 117 | if (ret != strlen(recvBuf)) 118 | std::cout << "send data error." << std::endl; 119 | 120 | std::cout << "send data to client successfully, data: " << recvBuf << std::endl; 121 | } 122 | else 123 | { 124 | std::cout << "recv data error." << std::endl; 125 | } 126 | 127 | //close(clientfd); 128 | clientfds.push_back(clientfd); 129 | } 130 | } 131 | 132 | //7.关闭侦听socket 133 | close(listenfd); 134 | 135 | return 0; 136 | } 137 | ``` 138 | 139 | 修改后的客户端代码如下: 140 | 141 | ``` 142 | /** 143 | * TCP客户端通信基本流程 144 | * zhangyl 2018.12.13 145 | */ 146 | #include 147 | #include 148 | #include 149 | #include 150 | #include 151 | #include 152 | 153 | #define SERVER_ADDRESS "127.0.0.1" 154 | #define SERVER_PORT 3000 155 | #define SEND_DATA "helloworld" 156 | 157 | int main(int argc, char* argv[]) 158 | { 159 | //1.创建一个socket 160 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 161 | if (clientfd == -1) 162 | { 163 | std::cout << "create client socket error." << std::endl; 164 | return -1; 165 | } 166 | 167 | //2.连接服务器 168 | struct sockaddr_in serveraddr; 169 | serveraddr.sin_family = AF_INET; 170 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 171 | serveraddr.sin_port = htons(SERVER_PORT); 172 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 173 | { 174 | std::cout << "connect socket error." << std::endl; 175 | return -1; 176 | } 177 | 178 | //3. 向服务器发送数据 179 | int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); 180 | if (ret != strlen(SEND_DATA)) 181 | { 182 | std::cout << "send data error." << std::endl; 183 | return -1; 184 | } 185 | 186 | std::cout << "send data successfully, data: " << SEND_DATA << std::endl; 187 | 188 | //4. 从客户端收取数据 189 | char recvBuf[32] = {0}; 190 | ret = recv(clientfd, recvBuf, 32, 0); 191 | if (ret > 0) 192 | { 193 | std::cout << "recv data successfully, data: " << recvBuf << std::endl; 194 | } 195 | else 196 | { 197 | std::cout << "recv data error, data: " << recvBuf << std::endl; 198 | } 199 | 200 | //5. 关闭socket 201 | //close(clientfd); 202 | //这里仅仅是为了让客户端程序不退出 203 | while (true) 204 | { 205 | sleep(3); 206 | } 207 | 208 | return 0; 209 | } 210 | ``` 211 | 212 | 213 | 214 | 将程序编译好后(编译方法和上文一样),我们先启动server,再启动三个客户端。然后通过 **lsof** 命令查看当前机器上的 TCP 连接信息,为了更清楚地显示结果,已经将不相关的连接信息去掉了,结果如下所示: 215 | 216 | ``` 217 | [root@localhost ~]# lsof -i -Pn 218 | COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME 219 | server 1445 root 3u IPv4 21568 0t0 TCP *:3000 (LISTEN) 220 | server 1445 root 4u IPv4 21569 0t0 TCP 127.0.0.1:3000->127.0.0.1:40818 (ESTABLISHED) 221 | server 1445 root 5u IPv4 21570 0t0 TCP 127.0.0.1:3000->127.0.0.1:40820 (ESTABLISHED) 222 | server 1445 root 6u IPv4 21038 0t0 TCP 127.0.0.1:3000->127.0.0.1:40822 (ESTABLISHED) 223 | client 1447 root 3u IPv4 21037 0t0 TCP 127.0.0.1:40818->127.0.0.1:3000 (ESTABLISHED) 224 | client 1448 root 3u IPv4 21571 0t0 TCP 127.0.0.1:40820->127.0.0.1:3000 (ESTABLISHED) 225 | client 1449 root 3u IPv4 21572 0t0 TCP 127.0.0.1:40822->127.0.0.1:3000 (ESTABLISHED) 226 | ``` 227 | 228 | 上面的结果显示,**server** 进程(进程 ID 是 **1445**)在 **3000** 端口开启侦听,有三个 **client** 进程(进程 ID 分别是**1447**、**1448**、**1449**)分别通过端口号 **40818**、**40820**、**40822** 连到 **server** 进程上的,作为客户端的一方,端口号是系统随机分配的。 229 | 230 | - 情形二:客户端绑定端口号 **0** 231 | 232 | 服务器端代码保持不变,我们修改下客户端代码: 233 | 234 | ``` 235 | /** 236 | * TCP客户端通信基本流程 237 | * zhangyl 2018.12.13 238 | */ 239 | #include 240 | #include 241 | #include 242 | #include 243 | #include 244 | #include 245 | 246 | #define SERVER_ADDRESS "127.0.0.1" 247 | #define SERVER_PORT 3000 248 | #define SEND_DATA "helloworld" 249 | 250 | int main(int argc, char* argv[]) 251 | { 252 | //1.创建一个socket 253 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 254 | if (clientfd == -1) 255 | { 256 | std::cout << "create client socket error." << std::endl; 257 | return -1; 258 | } 259 | 260 | struct sockaddr_in bindaddr; 261 | bindaddr.sin_family = AF_INET; 262 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 263 | //将socket绑定到0号端口上去 264 | bindaddr.sin_port = htons(0); 265 | if (bind(clientfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 266 | { 267 | std::cout << "bind socket error." << std::endl; 268 | return -1; 269 | } 270 | 271 | //2.连接服务器 272 | struct sockaddr_in serveraddr; 273 | serveraddr.sin_family = AF_INET; 274 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 275 | serveraddr.sin_port = htons(SERVER_PORT); 276 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 277 | { 278 | std::cout << "connect socket error." << std::endl; 279 | return -1; 280 | } 281 | 282 | //3. 向服务器发送数据 283 | int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0); 284 | if (ret != strlen(SEND_DATA)) 285 | { 286 | std::cout << "send data error." << std::endl; 287 | return -1; 288 | } 289 | 290 | std::cout << "send data successfully, data: " << SEND_DATA << std::endl; 291 | 292 | //4. 从客户端收取数据 293 | char recvBuf[32] = {0}; 294 | ret = recv(clientfd, recvBuf, 32, 0); 295 | if (ret > 0) 296 | { 297 | std::cout << "recv data successfully, data: " << recvBuf << std::endl; 298 | } 299 | else 300 | { 301 | std::cout << "recv data error, data: " << recvBuf << std::endl; 302 | } 303 | 304 | //5. 关闭socket 305 | //close(clientfd); 306 | //这里仅仅是为了让客户端程序不退出 307 | while (true) 308 | { 309 | sleep(3); 310 | } 311 | 312 | return 0; 313 | } 314 | ``` 315 | 316 | 我们再次编译客户端程序,并启动三个 **client** 进程,然后用 **lsof** 命令查看机器上的 TCP 连接情况,结果如下所示: 317 | 318 | ``` 319 | [root@localhost ~]# lsof -i -Pn 320 | COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME 321 | server 1593 root 3u IPv4 21807 0t0 TCP *:3000 (LISTEN) 322 | server 1593 root 4u IPv4 21808 0t0 TCP 127.0.0.1:3000->127.0.0.1:44220 (ESTABLISHED) 323 | server 1593 root 5u IPv4 19311 0t0 TCP 127.0.0.1:3000->127.0.0.1:38990 (ESTABLISHED) 324 | server 1593 root 6u IPv4 21234 0t0 TCP 127.0.0.1:3000->127.0.0.1:42365 (ESTABLISHED) 325 | client 1595 root 3u IPv4 22626 0t0 TCP 127.0.0.1:44220->127.0.0.1:3000 (ESTABLISHED) 326 | client 1611 root 3u IPv4 21835 0t0 TCP 127.0.0.1:38990->127.0.0.1:3000 (ESTABLISHED) 327 | client 1627 root 3u IPv4 21239 0t0 TCP 127.0.0.1:42365->127.0.0.1:3000 (ESTABLISHED) 328 | ``` 329 | 330 | 通过上面的结果,我们发现三个 **client** 进程使用的端口号仍然是系统随机分配的,也就是说绑定 **0** 号端口和没有绑定效果是一样的。 331 | 332 | 333 | 334 | - 情形三:客户端绑定一个固定端口 335 | 336 | 我们这里使用 **20000** 端口,当然读者可以根据自己的喜好选择,只要保证所选择的端口号当前没有被其他程序占用即可,服务器代码保持不变,客户端绑定代码中的端口号从 **0** 改成 **20000**。这里为了节省篇幅,只贴出修改处的代码: 337 | 338 | ``` 339 | struct sockaddr_in bindaddr; 340 | bindaddr.sin_family = AF_INET; 341 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 342 | //将socket绑定到20000号端口上去 343 | bindaddr.sin_port = htons(20000); 344 | if (bind(clientfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 345 | { 346 | std::cout << "bind socket error." << std::endl; 347 | return -1; 348 | } 349 | ``` 350 | 351 | 再次重新编译程序,先启动一个客户端后,我们看到此时的 TCP 连接状态: 352 | 353 | ``` 354 | [root@localhost testsocket]# lsof -i -Pn 355 | COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME 356 | server 1676 root 3u IPv4 21933 0t0 TCP *:3000 (LISTEN) 357 | server 1676 root 4u IPv4 21934 0t0 TCP 127.0.0.1:3000->127.0.0.1:20000 (ESTABLISHED) 358 | client 1678 root 3u IPv4 21336 0t0 TCP 127.0.0.1:20000->127.0.0.1:3000 (ESTABLISHED) 359 | ``` 360 | 361 | 通过上面的结果,我们发现 **client** 进程确实使用 **20000** 号端口连接到 **server** 进程上去了。这个时候如果我们再开启一个 **client** 进程,我们猜想由于端口号 **20000** 已经被占用,新启动的 **client** 会由于调用 **bind** 函数出错而退出,我们实际验证一下: 362 | 363 | ``` 364 | [root@localhost testsocket]# ./client 365 | bind socket error. 366 | [root@localhost testsocket]# 367 | ``` 368 | 369 | 结果确实和我们预想的一样。 370 | 371 | 372 | 在技术面试的时候,有时候面试官会问 TCP 网络通信的客户端程序中的 socket 是否可以调用 bind 函数,相信读到这里,聪明的读者已经有答案了。 373 | 374 | 另外,Linux 的 **nc** 命令有个 **-p** 选项(字母 **p** 是小写),这个选项的作用就是 **nc** 在模拟客户端程序时,可以使用指定端口号连接到服务器程序上去,实现原理相信读者也明白了。我们还是以上面的服务器程序为例,这个我们不用我们的 **client** 程序,改用 **nc** 命令来模拟客户端。在 **shell** 终端输入: 375 | 376 | ``` 377 | [root@localhost testsocket]# nc -v -p 9999 127.0.0.1 3000 378 | Ncat: Version 6.40 ( http://nmap.org/ncat ) 379 | Ncat: Connected to 127.0.0.1:3000. 380 | My name is zhangxf 381 | My name is zhangxf 382 | ``` 383 | 384 | **-v** 选项表示输出 **nc** 命令连接的详细信息,这里连接成功以后,会输出“**Ncat: Connected to 127.0.0.1:3000.**” 提示已经连接到服务器的 **3000** 端口上去了。 385 | 386 | **-p** 选项的参数值是 **9999** 表示,我们要求 **nc** 命令本地以端口号 **9999** 连接服务器,注意不要与端口号 **3000** 混淆,**3000** 是服务器的侦听端口号,也就是我们的连接的目标端口号,**9999** 是我们客户端使用的端口号。我们用 **lsof** 命令来验证一下我们的 **nc** 命令是否确实以 **9999** 端口号连接到 **server** 进程上去了。 387 | 388 | ``` 389 | [root@localhost testsocket]# lsof -i -Pn 390 | COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME 391 | server 1676 root 3u IPv4 21933 0t0 TCP *:3000 (LISTEN) 392 | server 1676 root 7u IPv4 22405 0t0 TCP 127.0.0.1:3000->127.0.0.1:9999 (ESTABLISHED) 393 | nc 2005 root 3u IPv4 22408 0t0 TCP 127.0.0.1:9999->127.0.0.1:3000 (ESTABLISHED) 394 | ``` 395 | 396 | 结果确实如我们期望的一致。 397 | 398 | 当然,我们用 **nc** 命令连接上 **server** 进程以后,我们还给服务器发了一条消息"**My name is zhangxf**",**server** 程序收到消息后把这条消息原封不动地返还给我们,以下是 **server** 端运行结果: 399 | 400 | ``` 401 | [root@localhost testsocket]# ./server 402 | recv data from client, data: My name is zhangxf 403 | 404 | send data to client successfully, data: My name is zhangxf 405 | ``` 406 | 407 | 关于 **lsof** 和 **nc** 命令我们将会在《**网络通信故障排查常用命令**》专题中详细介绍。 408 | 409 | 410 | 411 | 412 | 413 | ------ 414 | 415 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 416 | 417 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 418 | 419 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /网络通信基础重难点解析 12 :Linux epoll 模型.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 12 :Linux epoll 模型 2 | 3 | 4 | 5 | ### Linux epoll 模型 6 | 7 | 综合 **select** 和 **poll** 的一些优缺点,Linux 从内核 2.6 版本开始引入了更高效的 epoll 模型,本节我们来详细介绍 epoll 模型。 8 | 9 | 要想使用 epoll 模型,必须先需要创建一个 epollfd,这需要使用 **epoll_create** 函数去创建: 10 | 11 | ``` 12 | #include 13 | 14 | int epoll_create(int size); 15 | ``` 16 | 17 | 参数 **size** 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值。**epoll_create** 函数调用成功返回一个非负值的 epollfd,调用失败返回 -1。 18 | 19 | 有了 epollfd 之后,我们需要将我们需要检测事件的其他 fd 绑定到这个 epollfd 上,或者修改一个已经绑定上去的 fd 的事件类型,或者在不需要时将 fd 从 epollfd 上解绑,这都可以使用 **epoll_ctl** 函数: 20 | 21 | ``` 22 | int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event); 23 | ``` 24 | 25 | **参数说明:** 26 | 27 | - 参数 **epfd** 即上文提到的 epollfd; 28 | 29 | - 参数 **op**,操作类型,取值有 **EPOLL_CTL_ADD**、**EPOLL_CTL_MOD** 和 **EPOLL_CTL_DEL**,分别表示向 epollfd 上添加、修改和移除一个其他 fd,当取值是 **EPOLL_CTL_DEL**,第四个参数 **event** 忽略不计,可以设置为 NULL; 30 | 31 | - 参数 **fd**,即需要被操作的 fd; 32 | 33 | - 参数 **event**,这是一个 **epoll_event** 结构体的地址,**epoll_event** 结构体定义如下: 34 | 35 | ``` 36 | struct epoll_event 37 | { 38 | uint32_t events; /* 需要检测的 fd 事件,取值与 poll 函数一样 */ 39 | epoll_data_t data; /* 用户自定义数据 */ 40 | }; 41 | ``` 42 | 43 | **epoll_event** 结构体的 **data** 字段的类型是 **epoll_data_t**,我们可以利用这个字段设置一个自己的自定义数据,它本质上是一个 Union 对象,在 64 位操作系统中其大小是 8 字节,其定义如下: 44 | 45 | ``` 46 | typedef union epoll_data 47 | { 48 | void* ptr; 49 | int fd; 50 | uint32_t u32; 51 | uint64_t u64; 52 | } epoll_data_t; 53 | ``` 54 | 55 | - **函数返回值**:**epoll_ctl** 调用成功返回 0,调用失败返回 -1,你可以通过 **errno** 错误码获取具体的错误原因。 56 | 57 | 58 | 59 | 创建了 epollfd,设置好某个 fd 上需要检测事件并将该 fd 绑定到 epollfd 上去后,我们就可以调用 **epoll_wait** 检测事件了,**epoll_wait** 函数签名如下: 60 | 61 | ``` 62 | int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); 63 | ``` 64 | 65 | 参数的形式和 **poll** 函数很类似,参数 **events** 是一个 **epoll_event** 结构数组的首地址,这是一个输出参数,函数调用成功后,**events** 中存放的是与就绪事件相关 **epoll_event** 结构体数组;参数 **maxevents** 是数组元素的个数;**timeout** 是超时时间,单位是毫秒,如果设置为 0,**epoll_wait** 会立即返回。 66 | 67 | 当 **epoll_wait** 调用成功会返回有事件的 fd 数目;如果返回 0 表示超时;调用失败返回 -1。 68 | 69 | **epoll_wait** 使用示例如下: 70 | 71 | ``` 72 | while (true) 73 | { 74 | epoll_event epoll_events[1024]; 75 | int n = epoll_wait(epollfd, epoll_events, 1024, 1000); 76 | if (n < 0) 77 | { 78 | //被信号中断 79 | if (errno == EINTR) 80 | continue; 81 | 82 | //出错,退出 83 | break; 84 | } 85 | else if (n == 0) 86 | { 87 | //超时,继续 88 | continue; 89 | } 90 | 91 | for (size_t i = 0; i < n; ++i) 92 | { 93 | // 处理可读事件 94 | if (epoll_events[i].events & POLLIN) 95 | { 96 | } 97 | // 处理可写事件 98 | else if (epoll_events[i].events & POLLOUT) 99 | { 100 | } 101 | //处理出错事件 102 | else if (epoll_events[i].events & POLLERR) 103 | { 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | 110 | 111 | 112 | 113 | #### epoll_wait 与 poll 的区别 114 | 115 | 通过前面介绍 **poll** 与 **epoll_wait** 函数的介绍,我们可以发现: 116 | 117 | **epoll_wait** 函数调用完之后,我们可以直接在 **event** 参数中拿到所有有事件就绪的 fd,直接处理即可(**event** 参数仅仅是个出参);而 **poll** 函数的事件集合调用前后数量都未改变,只不过调用前我们通过 **pollfd** 结构体的 **events** 字段设置待检测事件,调用后我们需要通过 **pollfd** 结构体的 **revents** 字段去检测就绪的事件( 参数 **fds** 既是入参也是出参)。 118 | 119 | 举个生活中的例子,某人不断给你一些苹果,这些苹果有生有熟,调用 **epoll_wait** 相当于: 120 | 121 | ``` 122 | 1. 你把苹果挨个投入到 epoll 机器中(调用 epoll_ctl); 123 | 2. 调用 epoll_wait 加工,你直接通过另外一个袋子就能拿到所有熟苹果。 124 | ``` 125 | 126 | 调用 **poll** 相当于: 127 | 128 | ``` 129 | 1. 把收到的苹果装入一个袋子里面然后调用 poll 加工; 130 | 2. 调用结束后,拿到原来的袋子,袋子中还是原来那么多苹果,只不过熟苹果被贴上了标签纸,你还是需要挨个去查看标签纸挑选熟苹果。 131 | ``` 132 | 133 | 当然,这并不意味着,**poll** 函数的效率不如 **epoll_wait**,一般在 fd 数量比较多,但某段时间内,就绪事件 fd 数量较少的情况下,**epoll_wait** 才会体现出它的优势,也就是说 socket 连接数量较大时而活跃连接较少时 epoll 模型更高效。 134 | 135 | 136 | 137 | #### LT 模式和 ET 模式 138 | 139 | 与 poll 的事件宏相比,epoll 新增了一个事件宏 **EPOLLET**,这就是所谓的**边缘触发模式**(**E**dge **T**rigger,ET),而默认的模式我们称为 **水平触发模式**(**L**evel **T**rigger,LT)。这两种模式的区别在于: 140 | 141 | - 对于水平触发模式,一个事件只要有,就会一直触发; 142 | - 对于边缘触发模式,只有一个事件从无到有才会触发。 143 | 144 | 这两个词汇来自电学术语,你可以将 fd 上有数据认为是**高电平**,没有数据认为是**低电平**,将 fd 可写认为是**高电平**,fd 不可写认为是**低电平**。那么水平模式的触发条件是状态处于高电平,而边缘模式的触发条件是新来一次电信号将当前状态变为高电平,即: 145 | 146 | **水平模式的触发条件** 147 | 148 | ``` 149 | 1. 低电平 => 高电平 150 | 2. 处于高电平状态 151 | ``` 152 | 153 | **边缘模式的触发条件** 154 | 155 | ``` 156 | 1. 低电平 => 高电平 157 | ``` 158 | 159 | 说的有点抽象,以 socket 的读事件为例,对于水平模式,只要 socket 上有未读完的数据,就会一直产生 POLLIN 事件;而对于边缘模式,socket 上每新来一次数据就会触发一次,如果上一次触发后,未将 socket 上的数据读完,也不会再触发,除非再新来一次数据。对于 socket 写事件,如果 socket 的 TCP 窗口一直不饱和,会一直触发 POLLOUT 事件;而对于边缘模式,只会触发一次,除非 TCP 窗口由不饱和变成饱和再一次变成不饱和,才会再次触发 POLLOUT 事件。 160 | 161 | **socket 可读事件水平模式触发条件:** 162 | 163 | ``` 164 | 1. socket上无数据 => socket上有数据 165 | 2. socket处于有数据状态 166 | ``` 167 | 168 | **socket 可读事件边缘模式触发条件:** 169 | 170 | ``` 171 | 1. socket上无数据 => socket上有数据 172 | 2. socket又新来一次数据 173 | ``` 174 | 175 | 176 | 177 | **socket 可写事件水平模式触发条件:** 178 | 179 | ``` 180 | 1. socket可写 => socket可写 181 | 2. socket不可写 => socket可写 182 | ``` 183 | 184 | **socket 可写事件边缘模式触发条件:** 185 | 186 | ``` 187 | 1. socket不可写 => socket可写 188 | ``` 189 | 190 | 191 | 192 | 也就是说,如果对于一个非阻塞 socket,如果使用 epoll 边缘模式去检测数据是否可读,触发可读事件以后,一定要一次性把 socket 上的数据收取干净才行,也就是说一定要循环调用 recv 函数直到 recv 出错,错误码是**EWOULDBLOCK**(**EAGAIN** 一样)(此时表示 socket 上本次数据已经读完);如果使用水平模式,则不用,你可以根据业务一次性收取固定的字节数,或者收完为止。边缘模式下收取数据的代码写法示例如下: 193 | 194 | ``` 195 | bool TcpSession::RecvEtMode() 196 | { 197 | //每次只收取256个字节 198 | char buff[256]; 199 | while (true) 200 | { 201 | int nRecv = ::recv(clientfd_, buff, 256, 0); 202 | if (nRecv == -1) 203 | { 204 | if (errno == EWOULDBLOCK) 205 | return true; 206 | else if (errno == EINTR) 207 | continue; 208 | 209 | return false; 210 | } 211 | //对端关闭了socket 212 | else if (nRecv == 0) 213 | return false; 214 | 215 | inputBuffer_.add(buff, (size_t)nRecv); 216 | } 217 | 218 | return true; 219 | } 220 | ``` 221 | 222 | 223 | 224 | 下面我们来看一个具体的例子,代码如下: 225 | 226 | ``` 227 | /** 228 | * 验证epoll的LT与ET模式的区别, epoll_server.cpp 229 | * zhangyl 2019.04.01 230 | */ 231 | #include 232 | #include 233 | #include 234 | #include 235 | #include 236 | #include 237 | #include 238 | #include 239 | #include 240 | #include 241 | #include 242 | #include 243 | 244 | int main() 245 | { 246 | //创建一个监听socket 247 | int listenfd = socket(AF_INET, SOCK_STREAM, 0); 248 | if (listenfd == -1) 249 | { 250 | std::cout << "create listen socket error" << std::endl; 251 | return -1; 252 | } 253 | 254 | //设置重用ip地址和端口号 255 | int on = 1; 256 | setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on)); 257 | setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on)); 258 | 259 | 260 | //将监听socker设置为非阻塞的 261 | int oldSocketFlag = fcntl(listenfd, F_GETFL, 0); 262 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 263 | if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) 264 | { 265 | close(listenfd); 266 | std::cout << "set listenfd to nonblock error" << std::endl; 267 | return -1; 268 | } 269 | 270 | //初始化服务器地址 271 | struct sockaddr_in bindaddr; 272 | bindaddr.sin_family = AF_INET; 273 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 274 | bindaddr.sin_port = htons(3000); 275 | 276 | if (bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) == -1) 277 | { 278 | std::cout << "bind listen socker error." << std::endl; 279 | close(listenfd); 280 | return -1; 281 | } 282 | 283 | //启动监听 284 | if (listen(listenfd, SOMAXCONN) == -1) 285 | { 286 | std::cout << "listen error." << std::endl; 287 | close(listenfd); 288 | return -1; 289 | } 290 | 291 | 292 | //创建epollfd 293 | int epollfd = epoll_create(1); 294 | if (epollfd == -1) 295 | { 296 | std::cout << "create epollfd error." << std::endl; 297 | close(listenfd); 298 | return -1; 299 | } 300 | 301 | epoll_event listen_fd_event; 302 | listen_fd_event.data.fd = listenfd; 303 | listen_fd_event.events = EPOLLIN; 304 | //取消注释掉这一行,则使用ET模式 305 | //listen_fd_event.events |= EPOLLET; 306 | 307 | //将监听sokcet绑定到epollfd上去 308 | if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1) 309 | { 310 | std::cout << "epoll_ctl error" << std::endl; 311 | close(listenfd); 312 | return -1; 313 | } 314 | 315 | int n; 316 | while (true) 317 | { 318 | epoll_event epoll_events[1024]; 319 | n = epoll_wait(epollfd, epoll_events, 1024, 1000); 320 | if (n < 0) 321 | { 322 | //被信号中断 323 | if (errno == EINTR) 324 | continue; 325 | 326 | //出错,退出 327 | break; 328 | } 329 | else if (n == 0) 330 | { 331 | //超时,继续 332 | continue; 333 | } 334 | for (size_t i = 0; i < n; ++i) 335 | { 336 | //事件可读 337 | if (epoll_events[i].events & EPOLLIN) 338 | { 339 | if (epoll_events[i].data.fd == listenfd) 340 | { 341 | //侦听socket,接受新连接 342 | struct sockaddr_in clientaddr; 343 | socklen_t clientaddrlen = sizeof(clientaddr); 344 | int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddrlen); 345 | if (clientfd != -1) 346 | { 347 | int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); 348 | int newSocketFlag = oldSocketFlag | O_NONBLOCK; 349 | if (fcntl(clientfd, F_SETFD, newSocketFlag) == -1) 350 | { 351 | close(clientfd); 352 | std::cout << "set clientfd to nonblocking error." << std::endl; 353 | } 354 | else 355 | { 356 | epoll_event client_fd_event; 357 | client_fd_event.data.fd = clientfd; 358 | client_fd_event.events = EPOLLIN; 359 | //取消注释这一行,则使用ET模式 360 | //client_fd_event.events |= EPOLLET; 361 | if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1) 362 | { 363 | std::cout << "new client accepted,clientfd: " << clientfd << std::endl; 364 | } 365 | else 366 | { 367 | std::cout << "add client fd to epollfd error" << std::endl; 368 | close(clientfd); 369 | } 370 | } 371 | } 372 | } 373 | else 374 | { 375 | std::cout << "client fd: " << epoll_events[i].data.fd << " recv data." << std::endl; 376 | //普通clientfd 377 | char ch; 378 | //每次只收一个字节 379 | int m = recv(epoll_events[i].data.fd, &ch, 1, 0); 380 | if (m == 0) 381 | { 382 | //对端关闭了连接,从epollfd上移除clientfd 383 | if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) 384 | { 385 | std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl; 386 | } 387 | close(epoll_events[i].data.fd); 388 | } 389 | else if (m < 0) 390 | { 391 | //出错 392 | if (errno != EWOULDBLOCK && errno != EINTR) 393 | { 394 | if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) 395 | { 396 | std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl; 397 | } 398 | close(epoll_events[i].data.fd); 399 | } 400 | } 401 | else 402 | { 403 | //正常收到数据 404 | std::cout << "recv from client:" << epoll_events[i].data.fd << ", " << ch << std::endl; 405 | } 406 | } 407 | } 408 | else if (epoll_events[i].events & POLLERR) 409 | { 410 | // TODO 暂不处理 411 | } 412 | } 413 | } 414 | 415 | close(listenfd); 416 | return 0; 417 | } 418 | 419 | ``` 420 | 421 | 我们先来看水平模式的行为,将代码 **79** 行和 **134** 行注释掉则使用 **LT 模式**,我们编译下程序并运行: 422 | 423 | ``` 424 | [root@localhost testepoll]# g++ -g -o epoll_server epoll_server.cpp 425 | [root@localhost testepoll]# ./epoll_server 426 | ``` 427 | 428 | 然后再另外开启一个 shell 窗口,使用 nc 命令模拟一个客户端,连接服务器成功后,我们给服务器发送一个消息"**abcef**": 429 | 430 | ``` 431 | [root@localhost ~]# nc -v 127.0.0.1 3000 432 | Ncat: Version 7.50 ( https://nmap.org/ncat ) 433 | Ncat: Connected to 127.0.0.1:3000. 434 | abcdef 435 | 436 | ``` 437 | 438 | 此时服务器端输出: 439 | 440 | ``` 441 | [root@localhost testepoll]# ./epoll_server 442 | new client accepted,clientfd: 5 443 | client fd: 5 recv data. 444 | recv from client:5, a 445 | client fd: 5 recv data. 446 | recv from client:5, b 447 | client fd: 5 recv data. 448 | recv from client:5, c 449 | client fd: 5 recv data. 450 | recv from client:5, d 451 | client fd: 5 recv data. 452 | recv from client:5, e 453 | client fd: 5 recv data. 454 | recv from client:5, f 455 | client fd: 5 recv data. 456 | recv from client:5, 457 | 458 | ``` 459 | 460 | **nc** 命令实际发送了 **a**、**b**、**c**、**d**、**e**、**f** 和 **\n** 七个字符,由于服务器端使用的是 **LT 模式**,每次接收一个字符,只要 socket 接收缓冲区中仍有数据可读,POLLIN 事件就会一直触发,所以服务器一共有 7 次输出,直到 socket 接收缓冲区没有数据为止。 461 | 462 | 我们将代码 **79** 行和 **134** 行注释取消掉,使用 **ET 模式**再试一下,修改代码并重新编译,然后重新运行一下。再次使用 **nc** 命令模拟一个客户端连接后发送"**abcef**",服务器只会有一次输出,效果如下: 463 | 464 | ![](http://www.hootina.org/github_easyserverdev/20190401134302.png) 465 | 466 | 467 | 468 | 由于使用了 **ET 模式**,只会触发一次 POLLIN 事件,如果此时没有新数据到来,就再也不会触发。所以,如果我们继续给服务器发送一条新数据,如 **123**,服务器将再次触发一次 POLLIN 事件,然后打印出字母 **b**,效果如下: 469 | 470 | ![](http://www.hootina.org/github_easyserverdev/20190401134555.png) 471 | 472 | ![](D:/mycode/mybook/Chapter04/20190401134631.png) 473 | 474 | 475 | 476 | 所以如果使用 **ET 模式**,切记要将该次 socket 上的数据收完。 477 | 478 | 479 | 480 | > **小结:**LT 模式和 ET 模式各有优缺点,无所谓孰优孰劣。使用 LT 模式,我们可以自由决定每次收取多少字节(对于普通 socket)或何时接收连接(对于侦听 socket),但是可能会导致多次触发;使用 ET 模式,我们必须每次都要将数据收完(对于普通 socket)或必须理解调用 accept 接收连接(对于侦听socket),其优点是触发次数少。 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | ------ 489 | 490 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 491 | 492 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 493 | 494 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) -------------------------------------------------------------------------------- /网络通信基础重难点解析 04 :select 函数用法.md: -------------------------------------------------------------------------------- 1 | ## 网络通信基础重难点解析 04 :select 函数用法 2 | 3 | 4 | 5 | ### select 函数用法 6 | 7 | **select** 函数是网络通信编程中非常常用的一个函数,因此应该熟练掌握它。虽然它是 BSD 标准之一的 Socket 函数之一,但在 Linux 和 Windows 平台,其行为表现还是有点区别的。我们先来看一下 Linux 平台上的 select 函数。 8 | 9 | #### Linux 平台下的 select 函数 10 | 11 | **select** 函数的作用是检测一组 socket 中某个或某几个是否有“**事件**”就绪,这里的“**事件**”一般分为如下三类: 12 | 13 | - **读事件就绪**: 14 | 15 | > 1. socket 内核中,接收缓冲区中的字节数大于等于低水位标记 SO_RCVLOWAT,此时调用 **recv** 或 **read** 函数可以无阻塞的读该文件描述符, 并且返回值大于0; 16 | > 2. TCP 连接的对端关闭连接,此时调用 **recv** 或 **read** 函数对该 socket 读,则返回 0; 17 | > 3. 侦听 socket 上有新的连接请求; 18 | > 4. socket 上有未处理的错误。 19 | 20 | - **写事件就绪:** 21 | 22 | >1. socket 内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩) 大于等于低水位标记 SO_SNDLOWAT,此时可以无阻塞的写, 并且返回值大于0; 23 | >2. socket 的写操作被关闭(调用了 close 或者 shutdown 函数)( 对一个写操作被关闭的 socket 进行写操作, 会触发 SIGPIPE 信号); 24 | >3. socket 使⽤非阻塞 connect 连接成功或失败之后; 25 | 26 | - **异常事件就绪** 27 | 28 | > socket 上收到带外数据。 29 | 30 | 函数签名如下: 31 | 32 | ``` 33 | int select(int nfds, 34 | fd_set *readfds, 35 | fd_set *writefds, 36 | fd_set *exceptfds, 37 | struct timeval *timeout); 38 | ``` 39 | 40 | 参数说明: 41 | 42 | - 参数 **nfds**, Linux 下 socket 也称 fd,这个参数的值设置成所有需要使用 select 函数监听的 fd 中最大 fd 值加 1。 43 | 44 | - 参数 **readfds**,需要监听可读事件的 fd 集合。 45 | 46 | - 参数 **writefds**,需要监听可写事件的 fd 集合。 47 | 48 | - 参数 **exceptfds**,需要监听异常事件 fd 集合。 49 | 50 | **readfds**、**writefds** 和 **exceptfds** 类型都是 **fd_set**,这是一个结构体信息,其定义位于 **/usr/include/sys/select.h** 中: 51 | 52 | ``` 53 | /* The fd_set member is required to be an array of longs. */ 54 | typedef long int __fd_mask; 55 | 56 | /* Some versions of define this macros. */ 57 | #undef __NFDBITS 58 | /* It's easier to assume 8-bit bytes than to get CHAR_BIT. */ 59 | #define __NFDBITS (8 * (int) sizeof (__fd_mask)) 60 | #define __FD_ELT(d) ((d) / __NFDBITS) 61 | #define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) 62 | 63 | /* fd_set for select and pselect. */ 64 | typedef struct 65 | { 66 | /* XPG4.2 requires this member name. Otherwise avoid the name 67 | from the global namespace. */ 68 | #ifdef __USE_XOPEN 69 | __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; 70 | # define __FDS_BITS(set) ((set)->fds_bits) 71 | #else 72 | // 在我的centOS 7.0 系统中的值: 73 | // __FD_SETSIZE = 1024 74 | //__NFDBITS = 64 75 | __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; 76 | # define __FDS_BITS(set) ((set)->__fds_bits) 77 | #endif 78 | } fd_set; 79 | 80 | /* Maximum number of file descriptors in `fd_set'. */ 81 | #define FD_SETSIZE __FD_SETSIZE 82 | ``` 83 | 84 | 我们假设未定义宏 **__USE_XOPEN**,将上面的代码整理一下: 85 | 86 | ``` 87 | typedef struct 88 | { 89 | long int __fds_bits[16]; 90 | } fd_set; 91 | ``` 92 | 93 | 将一个 fd 添加到 fd_set 这个集合中需要使用 **FD_SET** 宏,其定义如下: 94 | 95 | ``` 96 | void FD_SET(int fd, fd_set *set); 97 | ``` 98 | 99 | 其实现如下: 100 | 101 | ``` 102 | #define FD_SET(fd,fdsetp) __FD_SET(fd,fdsetp) 103 | ``` 104 | 105 | **FD_SET** 在内部又是通过宏 **__FD_SET** 来实现的,**__FD_SET** 的定义如下(位于 **/usr/include/bits/select.h** 中): 106 | 107 | ``` 108 | #if defined __GNUC__ && __GNUC__ >= 2 109 | 110 | # if __WORDSIZE == 64 111 | # define __FD_ZERO_STOS "stosq" 112 | # else 113 | # define __FD_ZERO_STOS "stosl" 114 | # endif 115 | 116 | # define __FD_ZERO(fdsp) \ 117 | do { \ 118 | int __d0, __d1; \ 119 | __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS \ 120 | : "=c" (__d0), "=D" (__d1) \ 121 | : "a" (0), "0" (sizeof (fd_set) \ 122 | / sizeof (__fd_mask)), \ 123 | "1" (&__FDS_BITS (fdsp)[0]) \ 124 | : "memory"); \ 125 | } while (0) 126 | 127 | #else /* ! GNU CC */ 128 | 129 | /* We don't use `memset' because this would require a prototype and 130 | the array isn't too big. */ 131 | # define __FD_ZERO(set) \ 132 | do { \ 133 | unsigned int __i; \ 134 | fd_set *__arr = (set); \ 135 | for (__i = 0; __i < sizeof (fd_set) / sizeof (__fd_mask); ++__i) \ 136 | __FDS_BITS (__arr)[__i] = 0; \ 137 | } while (0) 138 | 139 | #endif /* GNU CC */ 140 | 141 | #define __FD_SET(d, set) \ 142 | ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) 143 | #define __FD_CLR(d, set) \ 144 | ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d))) 145 | #define __FD_ISSET(d, set) \ 146 | ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0) 147 | ``` 148 | 149 | 重点看这一行: 150 | 151 | ``` 152 | ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d))) 153 | ``` 154 | 155 | **__FD_MASK** 和 **__FD_ELT** 宏在上面的代码中已经给出定义: 156 | 157 | ``` 158 | #define __FD_ELT(d) ((d) / __NFDBITS) 159 | #define __FD_MASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS)) 160 | ``` 161 | 162 | **__NFDBITS** 的值是 **64** (**8 * 8**),也就是说 **__FD_MASK (d) ** 先计算 fd 与 64 的余数 n,然后执行 1 << n,这一操作实际上是将 fd 的值放在 0~63 这 64 的位置上去,这个位置索引就是 fd 与 64 取模的结果;同理 **__FD_ELT(d)** 就是计算位置索引值了。举个例子,假设现在 fd 的 值是 57,那么在这 64 个位置的 57 位,其值在 64 个长度的二进制中置位是: 163 | 164 | ``` 165 | 0000 0010 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 166 | ``` 167 | 168 | 这个值就是 **1 << (57 % 64) **得到的数字。 169 | 170 | 但是前面 fd 数组的定义是: 171 | 172 | ``` 173 | typedef struct 174 | { 175 | long int __fds_bits[16]; //可以看成是128 bit的数组 176 | } fd_set; 177 | ``` 178 | 179 | **long int** 占 8 个字节,每个字节 8 bit,一共 16 个 **long int**,如果换成二进制的位( **bit** )就是 **8 * 8 * 16** = **1024**, 这说明在我的机器上,select 函数支持操作的最大 fd 数量是 1024。 180 | 181 | 182 | 183 | 同理,如果我们需要从 fd_set 上删除一个 fd,我们可以调用 **FD_CLR**,其定义如下: 184 | 185 | ``` 186 | void FD_CLR(int fd, fd_set *set); 187 | ``` 188 | 189 | 原理和 **FD_SET** 相同,即将对应标志清零即可。 190 | 191 | 如果,我们需要将 fd_set 中所有的 fd 都清掉,则使用宏 **FD_ZERO**: 192 | 193 | ``` 194 | void FD_ZERO(fd_set *set); 195 | ``` 196 | 197 | 当 select 函数返回时, 我们使用 **FD_ISSET** 宏来判断某个 fd 是否有我们关心的事件,**FD_ISSET** 宏的定义如下: 198 | 199 | ``` 200 | int FD_ISSET(int fd, fd_set *set); 201 | ``` 202 | 203 | **FD_ISSET** 宏本质上就是检测对应的位置上是否置 1,实现如下: 204 | 205 | ``` 206 | #define __FD_ISSET(d, set) \ 207 | ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0) 208 | ``` 209 | 210 | > 提醒一下: __FD_ELT 和 __FD_MASK 宏前文的代码已经给过具体实现了。 211 | 212 | - 参数 **timeout**,超时时间,即在这个参数设定的时间内检测这些 fd 的事件,超过这个时间后 **select** 函数将立即返回。这是一个 **timeval** 类型结构体,其定义如下: 213 | 214 | ``` 215 | struct timeval 216 | { 217 | long tv_sec; /* seconds */ 218 | long tv_usec; /* microseconds */ 219 | }; 220 | ``` 221 | 222 | **select** 函数的总超时时间是 **timeout->tv_sec** 和 **timeout->tv_usec** 之和, 前者的时间单位是秒,后者的时间单位是微秒。 223 | 224 | 225 | 说了这么多理论知识,我们先看一个具体的示例: 226 | 227 | ``` 228 | /** 229 | * select函数示例,server端, select_server.cpp 230 | * zhangyl 2018.12.24 231 | */ 232 | #include 233 | #include 234 | #include 235 | #include 236 | #include 237 | #include 238 | #include 239 | #include 240 | #include 241 | 242 | //自定义代表无效fd的值 243 | #define INVALID_FD -1 244 | 245 | int main(int argc, char* argv[]) 246 | { 247 | //创建一个侦听socket 248 | int listenfd = socket(AF_INET, SOCK_STREAM, 0); 249 | if (listenfd == -1) 250 | { 251 | std::cout << "create listen socket error." << std::endl; 252 | return -1; 253 | } 254 | 255 | //初始化服务器地址 256 | struct sockaddr_in bindaddr; 257 | bindaddr.sin_family = AF_INET; 258 | bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); 259 | bindaddr.sin_port = htons(3000); 260 | if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1) 261 | { 262 | std::cout << "bind listen socket error." << std::endl; 263 | close(listenfd); 264 | return -1; 265 | } 266 | 267 | //启动侦听 268 | if (listen(listenfd, SOMAXCONN) == -1) 269 | { 270 | std::cout << "listen error." << std::endl; 271 | close(listenfd); 272 | return -1; 273 | } 274 | 275 | //存储客户端socket的数组 276 | std::vector clientfds; 277 | int maxfd = listenfd; 278 | 279 | while (true) 280 | { 281 | fd_set readset; 282 | FD_ZERO(&readset); 283 | 284 | //将侦听socket加入到待检测的可读事件中去 285 | FD_SET(listenfd, &readset); 286 | 287 | //将客户端fd加入到待检测的可读事件中去 288 | int clientfdslength = clientfds.size(); 289 | for (int i = 0; i < clientfdslength; ++i) 290 | { 291 | if (clientfds[i] != INVALID_FD) 292 | { 293 | FD_SET(clientfds[i], &readset); 294 | } 295 | } 296 | 297 | timeval tm; 298 | tm.tv_sec = 1; 299 | tm.tv_usec = 0; 300 | //暂且只检测可读事件,不检测可写和异常事件 301 | int ret = select(maxfd + 1, &readset, NULL, NULL, &tm); 302 | if (ret == -1) 303 | { 304 | //出错,退出程序。 305 | if (errno != EINTR) 306 | break; 307 | } 308 | else if (ret == 0) 309 | { 310 | //select 函数超时,下次继续 311 | continue; 312 | } else { 313 | //检测到某个socket有事件 314 | if (FD_ISSET(listenfd, &readset)) 315 | { 316 | //侦听socket的可读事件,则表明有新的连接到来 317 | struct sockaddr_in clientaddr; 318 | socklen_t clientaddrlen = sizeof(clientaddr); 319 | //4. 接受客户端连接 320 | int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen); 321 | if (clientfd == -1) 322 | { 323 | //接受连接出错,退出程序 324 | break; 325 | } 326 | 327 | //只接受连接,不调用recv收取任何数据 328 | std:: cout << "accept a client connection, fd: " << clientfd << std::endl; 329 | clientfds.push_back(clientfd); 330 | //记录一下最新的最大fd值,以便作为下一轮循环中select的第一个参数 331 | if (clientfd > maxfd) 332 | maxfd = clientfd; 333 | } 334 | else 335 | { 336 | //假设对端发来的数据长度不超过63个字符 337 | char recvbuf[64]; 338 | int clientfdslength = clientfds.size(); 339 | for (int i = 0; i < clientfdslength; ++i) 340 | { 341 | if (clientfds[i] != -1 && FD_ISSET(clientfds[i], &readset)) 342 | { 343 | memset(recvbuf, 0, sizeof(recvbuf)); 344 | //非侦听socket,则接收数据 345 | int length = recv(clientfds[i], recvbuf, 64, 0); 346 | if (length <= 0 && errno != EINTR) 347 | { 348 | //收取数据出错了 349 | std::cout << "recv data error, clientfd: " << clientfds[i] << std::endl; 350 | close(clientfds[i]); 351 | //不直接删除该元素,将该位置的元素置位-1 352 | clientfds[i] = INVALID_FD; 353 | continue; 354 | } 355 | 356 | std::cout << "clientfd: " << clientfds[i] << ", recv data: " << recvbuf << std::endl; 357 | } 358 | } 359 | 360 | } 361 | } 362 | } 363 | 364 | //关闭所有客户端socket 365 | int clientfdslength = clientfds.size(); 366 | for (int i = 0; i < clientfdslength; ++i) 367 | { 368 | if (clientfds[i] != INVALID_FD) 369 | { 370 | close(clientfds[i]); 371 | } 372 | } 373 | 374 | //关闭侦听socket 375 | close(listenfd); 376 | 377 | return 0; 378 | } 379 | ``` 380 | 381 | 我们编译并运行程序: 382 | 383 | ``` 384 | [root@localhost testsocket]# g++ -g -o select_server select_server.cpp 385 | [root@localhost testsocket]# ./select_server 386 | ``` 387 | 388 | 然后,我们再多开几个 shell 窗口,我们这里不再专门编写客户端程序了,我们使用 Linux 下的 **nc** 指令模拟出两个客户端。 389 | 390 | shell 窗口1,连接成功以后发送字符串 **hello123**: 391 | 392 | ``` 393 | [root@localhost ~]# nc -v 127.0.0.1 3000 394 | Ncat: Version 6.40 ( http://nmap.org/ncat ) 395 | Ncat: Connected to 127.0.0.1:3000. 396 | hello123 397 | ``` 398 | 399 | shell 窗口2,连接成功以后发送字符串 **helloworld**: 400 | 401 | ``` 402 | [root@localhost ~]# nc -v 127.0.0.1 3000 403 | Ncat: Version 6.40 ( http://nmap.org/ncat ) 404 | Ncat: Connected to 127.0.0.1:3000. 405 | helloworld 406 | ``` 407 | 408 | 此时服务器端输出结果如下: 409 | 410 | ![](http://www.hootina.org/github_easyserverdev/20181224145113.png) 411 | 412 | 413 | 414 | 注意,由于 **nc** 发送的数据是按换行符来区分的,每一个数据包默认的换行符以**\n** 结束(当然,你可以 **-C** 选项换成\r\n),所以服务器收到数据后,显示出来的数据每一行下面都有一个空白行。 415 | 416 | 当断开各个客户端连接时,服务器端 select 函数对各个客户端 fd 检测时,仍然会触发可读事件,此时对这些 fd 调用 recv 函数会返回 **0**(recv 函数返回0,表明对端关闭了连接,这是一个很重要的知识点,下文我们会有一章节专门介绍这些函数的返回值),服务器端也关闭这些连接就可以了。 417 | 418 | 客户端断开连接后,服务器端的运行输出结果: 419 | 420 | ![](http://www.hootina.org/github_easyserverdev/20181224150008.png) 421 | 422 | 423 | 424 | 以上代码是一个简单的服务器程序实现的基本流程,代码虽然简单,但是非常具有典型性和代表性,而且同样适用于客户端网络通信,如果用于客户端的话,只需要用 select 检测连接 socket 就可以了,如果连接 socket 有可读事件,调用 recv 函数来接收数据,剩下的逻辑都是一样的。上面的代码我们画一张流程图如下: 425 | 426 | ![](http://www.hootina.org/github_easyserverdev/20181224162509.png) 427 | 428 | 429 | 430 | 431 | 432 | 关于上述代码在实际开发中有几个需要注意的事项,这里逐一来说明一下: 433 | 434 | 1. **select 函数调用前后会修改 readfds、writefds 和 exceptfds 这三个集合中的内容(如果有的话),所以如果您想下次调用 select 复用这个变量,记得在下次调用前再次调用 select 前先使用 FD_ZERO 将集合清零,然后调用 FD_SET 将需要检测事件的 fd 再次添加进去**。 435 | 436 | > select 函数调用之后,**readfds**、**writefds** 和 **exceptfds** 这三个集合中存放的不是我们之前设置进去的 fd,而是有相关有读写或异常事件的 fd,也就是说 select 函数会修改这三个参数的内容,这也要求我们**当一个 fd_set 被 select 函数调用后,这个 fd_set 就已经发生了改变,下次如果我们需要使用它,必须使用 FD_ZERO 宏先清零,再重新将我们关心的 fd 设置进去**。这点我们从 **FD_ISSET** 源码也可以看出来: 437 | 438 | ``` 439 | #define __FD_ISSET(d, set) \ 440 | ((__FDS_BITS (set)[__FD_ELT (d)] & __FD_MASK (d)) != 0) 441 | ``` 442 | 443 | 如果调用 select 函数之后没有改变 fd_set 集合,那么即使某个 socket 上没有事件,调用 select 函数之后我们用 **FD_ISSET** 检测,会原路得到原来设置上去的 socket。这是很多初学者在学习 select 函数容易犯的一个错误,我们通过一个示例来验证一下,这次我们把 select 函数用在客户端。 444 | 445 | ``` 446 | /** 447 | * 验证调用select后必须重设fd_set,select_client.cpp 448 | * zhangyl 2018.12.24 449 | */ 450 | #include 451 | #include 452 | #include 453 | #include 454 | #include 455 | #include 456 | #include 457 | #include 458 | 459 | #define SERVER_ADDRESS "127.0.0.1" 460 | #define SERVER_PORT 3000 461 | 462 | int main(int argc, char* argv[]) 463 | { 464 | //创建一个socket 465 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 466 | if (clientfd == -1) 467 | { 468 | std::cout << "create client socket error." << std::endl; 469 | return -1; 470 | } 471 | 472 | //连接服务器 473 | struct sockaddr_in serveraddr; 474 | serveraddr.sin_family = AF_INET; 475 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 476 | serveraddr.sin_port = htons(SERVER_PORT); 477 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 478 | { 479 | std::cout << "connect socket error." << std::endl; 480 | close(clientfd); 481 | return -1; 482 | } 483 | 484 | fd_set readset; 485 | FD_ZERO(&readset); 486 | 487 | //将侦听socket加入到待检测的可读事件中去 488 | FD_SET(clientfd, &readset); 489 | timeval tm; 490 | tm.tv_sec = 5; 491 | tm.tv_usec = 0; 492 | int ret; 493 | int count = 0; 494 | fd_set backup_readset; 495 | memcpy(&backup_readset, &readset, sizeof(fd_set)); 496 | while (true) 497 | { 498 | if (memcmp(&readset, &backup_readset, sizeof(fd_set)) == 0) 499 | { 500 | std::cout << "equal" << std::endl; 501 | } 502 | else 503 | { 504 | std::cout << "not equal" << std::endl; 505 | } 506 | 507 | //暂且只检测可读事件,不检测可写和异常事件 508 | ret = select(clientfd + 1, &readset, NULL, NULL, &tm); 509 | std::cout << "tm.tv_sec: " << tm.tv_sec << ", tm.tv_usec: " << tm.tv_usec << std::endl; 510 | if (ret == -1) 511 | { 512 | //除了被信号中断的情形,其他情况都是出错 513 | if (errno != EINTR) 514 | break; 515 | } else if (ret == 0){ 516 | //select函数超时 517 | std::cout << "no event in specific time interval, count:" << count << std::endl; 518 | ++count; 519 | continue; 520 | } else { 521 | if (FD_ISSET(clientfd, &readset)) 522 | { 523 | //检测到可读事件 524 | char recvbuf[32]; 525 | memset(recvbuf, 0, sizeof(recvbuf)); 526 | //假设对端发数据的时候不超过31个字符。 527 | int n = recv(clientfd, recvbuf, 32, 0); 528 | if (n < 0) 529 | { 530 | //除了被信号中断的情形,其他情况都是出错 531 | if (errno != EINTR) 532 | break; 533 | } else if (n == 0) { 534 | //对端关闭了连接 535 | break; 536 | } else { 537 | std::cout << "recv data: " << recvbuf << std::endl; 538 | } 539 | } 540 | else 541 | { 542 | std::cout << "other socket event." << std::endl; 543 | } 544 | } 545 | } 546 | 547 | //关闭socket 548 | close(clientfd); 549 | 550 | return 0; 551 | } 552 | ``` 553 | 554 | 555 | 556 | 在 shell 窗口输入以下命令编译程序产生可执行文件 **select_client**: 557 | 558 | ``` 559 | g++ -g -o select_client select_client.cpp 560 | ``` 561 | 562 | 这次产生的是客户端程序,服务器程序我们这里使用 Linux **nc** 命令来模拟一下,由于客户端连接的是 **127.0.0.1:3000** 这个地址和端口号,所以我们在另外一个shell 窗口的 **nc** 命令的参数可以这么写: 563 | 564 | ``` 565 | nc -v -l 0.0.0.0 3000 566 | ``` 567 | 568 | 执行效果如下: 569 | 570 | ![](http://www.hootina.org/github_easyserverdev/20181224232652.png) 571 | 572 | 573 | 574 | 接着我们启动客户端 **select_client**: 575 | 576 | ``` 577 | [root@myaliyun testsocket]# ./select_client 578 | ``` 579 | 580 | 581 | 582 | 需要注意的是,这里我故意将客户端代码中 select 函数的超时时间设置为5秒,以足够我们在这 5 秒内给客户端发一个数据。如果我们在 5 秒内给客户端发送 **hello** 字符串: 583 | 584 | ![](http://www.hootina.org/github_easyserverdev/20181224232942.png) 585 | 586 | 587 | 588 | 客户端输出如下: 589 | 590 | ``` 591 | [root@myaliyun testsocket]# ./select_client 592 | equal 593 | recv data: hello 594 | 595 | ...部分数据省略... 596 | not equal 597 | tm.tv_sec: 0, tm.tv_usec: 0 598 | no event in specific time interval, count:31454 599 | not equal 600 | tm.tv_sec: 0, tm.tv_usec: 0 601 | no event in specific time interval, count:31455 602 | not equal 603 | tm.tv_sec: 0, tm.tv_usec: 0 604 | no event in specific time interval, count:31456 605 | not equal 606 | tm.tv_sec: 0, tm.tv_usec: 0 607 | no event in specific time interval, count:31457 608 | ...部分输出省略... 609 | ``` 610 | 611 | 除了第一次 **select_client** 会输出 **equal** 字样,后面再也没输出,而 **select** 函数以后的执行结果也是超时,即使此时服务器端再次给客户端发送数据。因此验证了:**select 函数执行后,确实会对三个参数的 fd_set 进行修改** 。**select** 函数修改某个 fd_set 集合可以使用如下两张图来说明一下: 612 | 613 | ![](http://www.hootina.org/github_easyserverdev/20181224235756.png) 614 | 615 | ![](http://www.hootina.org/github_easyserverdev/20181225000010.png) 616 | 617 | 因此在调用 **select** 函数以后, 原来位置的的标志位可能已经不复存在,这也就是为什么我们的代码中调用一次 **select** 函数以后,即使服务器端再次发送数据过来,**select** 函数也不会再因为存在可读事件而返回了,因为第二次 clientfd 已经不在那个 read_set 中了。因此如果复用这些 fd_set 变量,必须按上文所说的重新清零再重新添加关心的 socket 到集合中去。 618 | 619 | 2. **select 函数也会修改 timeval 结构体的值,这也要求我们如果像复用这个变量,必须给 timeval 变量重新设置值。** 620 | 621 | 注意观察上面的例子的输出,我们在调用 **select** 函数一次之后,变量 tv 的值也被修改了。具体修改成多少,得看系统的表现。当然这种特性却不是跨平台的,在 Linux 系统中是这样的,而在其他操作系统上却不一定是这样(Windows 上就不会修改这个结构体的值),这点在 Linux man 手册 **select** 函数的说明中说的很清楚: 622 | 623 | ``` 624 | On Linux, select() modifies timeout to reflect the amount of time not slept; most other implementations do not do this.(POSIX.1-2001 permits either behavior.) This causes problems both when Linux code which reads timeout is ported to other operating systems, and when code is ported to Linux that reuses a struct timeval for multiple select()s in a loop without reinitializing it. Consider timeout to be undefined after select() returns. 625 | ``` 626 | 627 | 由于不同系统的实现不一样,man 手册的建议将 **select** 函数修改 **timeval** 结构体的值的行为当作是未定义的,言下之意是**如果你要下次使用 select 函数复用这个变量时,记得重新赋值**。这是 select 函数需要注意的第二个地方。 628 | 629 | 3. **select 函数的 timeval 结构体的 tv_sec 和 tv_sec 如果两个值设置为 0,即检测事件总时间设置为0,其行为是 select 会检测一下相关集合中的 fd,如果没有需要的事件,则立即返回**。 630 | 631 | 我们将上述 **select_client.cpp** 修改一下,修改后的代码如下: 632 | 633 | ``` 634 | /** 635 | * 验证select时间参数设置为0,select_client_tv0.cpp 636 | * zhangyl 2018.12.25 637 | */ 638 | #include 639 | #include 640 | #include 641 | #include 642 | #include 643 | #include 644 | #include 645 | #include 646 | 647 | #define SERVER_ADDRESS "127.0.0.1" 648 | #define SERVER_PORT 3000 649 | 650 | int main(int argc, char* argv[]) 651 | { 652 | //创建一个socket 653 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 654 | if (clientfd == -1) 655 | { 656 | std::cout << "create client socket error." << std::endl; 657 | return -1; 658 | } 659 | 660 | //连接服务器 661 | struct sockaddr_in serveraddr; 662 | serveraddr.sin_family = AF_INET; 663 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 664 | serveraddr.sin_port = htons(SERVER_PORT); 665 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 666 | { 667 | std::cout << "connect socket error." << std::endl; 668 | close(clientfd); 669 | return -1; 670 | } 671 | 672 | int ret; 673 | while (true) 674 | { 675 | fd_set readset; 676 | FD_ZERO(&readset); 677 | //将侦听socket加入到待检测的可读事件中去 678 | FD_SET(clientfd, &readset); 679 | timeval tm; 680 | tm.tv_sec = 0; 681 | tm.tv_usec = 0; 682 | 683 | //暂且只检测可读事件,不检测可写和异常事件 684 | ret = select(clientfd + 1, &readset, NULL, NULL, &tm); 685 | std::cout << "tm.tv_sec: " << tm.tv_sec << ", tm.tv_usec: " << tm.tv_usec << std::endl; 686 | if (ret == -1) 687 | { 688 | //除了被信号中断的情形,其他情况都是出错 689 | if (errno != EINTR) 690 | break; 691 | } else if (ret == 0){ 692 | //select函数超时 693 | std::cout << "no event in specific time interval." << std::endl; 694 | continue; 695 | } else { 696 | if (FD_ISSET(clientfd, &readset)) 697 | { 698 | //检测到可读事件 699 | char recvbuf[32]; 700 | memset(recvbuf, 0, sizeof(recvbuf)); 701 | //假设对端发数据的时候不超过31个字符。 702 | int n = recv(clientfd, recvbuf, 32, 0); 703 | if (n < 0) 704 | { 705 | //除了被信号中断的情形,其他情况都是出错 706 | if (errno != EINTR) 707 | break; 708 | } else if (n == 0) { 709 | //对端关闭了连接 710 | break; 711 | } else { 712 | std::cout << "recv data: " << recvbuf << std::endl; 713 | } 714 | } 715 | else 716 | { 717 | std::cout << "other socket event." << std::endl; 718 | } 719 | } 720 | } 721 | 722 | 723 | //关闭socket 724 | close(clientfd); 725 | 726 | return 0; 727 | } 728 | ``` 729 | 730 | 执行结果确实如我们预期的,这里 select 函数只是简单地检测一下 clientfd,并不会等待固定的时间,然后立即返回。 731 | 732 | ![](http://www.hootina.org/github_easyserverdev/20181225004159.png) 733 | 734 | 735 | 736 | 4. **如果将 select 函数的 timeval 参数设置为 NULL,则 select 函数会一直阻塞下去,直到我们需要的事件触发。** 737 | 738 | 我们将上述代码再修改一下: 739 | 740 | ``` 741 | /** 742 | * 验证select时间参数设置为NULL,select_client_tvnull.cpp 743 | * zhangyl 2018.12.25 744 | */ 745 | #include 746 | #include 747 | #include 748 | #include 749 | #include 750 | #include 751 | #include 752 | #include 753 | 754 | #define SERVER_ADDRESS "127.0.0.1" 755 | #define SERVER_PORT 3000 756 | 757 | int main(int argc, char* argv[]) 758 | { 759 | //创建一个socket 760 | int clientfd = socket(AF_INET, SOCK_STREAM, 0); 761 | if (clientfd == -1) 762 | { 763 | std::cout << "create client socket error." << std::endl; 764 | return -1; 765 | } 766 | 767 | //连接服务器 768 | struct sockaddr_in serveraddr; 769 | serveraddr.sin_family = AF_INET; 770 | serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS); 771 | serveraddr.sin_port = htons(SERVER_PORT); 772 | if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1) 773 | { 774 | std::cout << "connect socket error." << std::endl; 775 | close(clientfd); 776 | return -1; 777 | } 778 | 779 | int ret; 780 | while (true) 781 | { 782 | fd_set readset; 783 | FD_ZERO(&readset); 784 | //将侦听socket加入到待检测的可读事件中去 785 | FD_SET(clientfd, &readset); 786 | //timeval tm; 787 | //tm.tv_sec = 0; 788 | //tm.tv_usec = 0; 789 | 790 | //暂且只检测可读事件,不检测可写和异常事件 791 | ret = select(clientfd + 1, &readset, NULL, NULL, NULL); 792 | if (ret == -1) 793 | { 794 | //除了被信号中断的情形,其他情况都是出错 795 | if (errno != EINTR) 796 | break; 797 | } else if (ret == 0){ 798 | //select函数超时 799 | std::cout << "no event in specific time interval." << std::endl; 800 | continue; 801 | } else { 802 | if (FD_ISSET(clientfd, &readset)) 803 | { 804 | //检测到可读事件 805 | char recvbuf[32]; 806 | memset(recvbuf, 0, sizeof(recvbuf)); 807 | //假设对端发数据的时候不超过31个字符。 808 | int n = recv(clientfd, recvbuf, 32, 0); 809 | if (n < 0) 810 | { 811 | //除了被信号中断的情形,其他情况都是出错 812 | if (errno != EINTR) 813 | break; 814 | } else if (n == 0) { 815 | //对端关闭了连接 816 | break; 817 | } else { 818 | std::cout << "recv data: " << recvbuf << std::endl; 819 | } 820 | } 821 | else 822 | { 823 | std::cout << "other socket event." << std::endl; 824 | } 825 | } 826 | } 827 | 828 | 829 | //关闭socket 830 | close(clientfd); 831 | 832 | return 0; 833 | } 834 | ``` 835 | 836 | 837 | 838 | 我们先在另外一个 shell 窗口用 **nc** 命令模拟一个服务器,监听的 ip 地址和端口号是 **0.0.0.0:3000**: 839 | 840 | ``` 841 | [root@myaliyun ~]# nc -v -l 0.0.0.0 3000 842 | Ncat: Version 6.40 ( http://nmap.org/ncat ) 843 | Ncat: Listening on 0.0.0.0:3000 844 | ``` 845 | 846 | 然后回到原来的 shell 窗口,编译上述 **select_client_tvnull.cpp**,并使用 gdb 运行程序,这次使用 gdb 运行程序的目的是为了当程序“卡”在某个位置时,我们可以使用 Ctrl + C 把程序中断下来看看程序阻塞在哪个函数调用处: 847 | 848 | ``` 849 | [root@myaliyun testsocket]# g++ -g -o select_client_tvnull select_client_tvnull.cpp 850 | [root@myaliyun testsocket]# gdb select_client_tvnull 851 | Reading symbols from /root/testsocket/select_client_tvnull...done. 852 | (gdb) r 853 | Starting program: /root/testsocket/select_client_tvnull 854 | ^C 855 | Program received signal SIGINT, Interrupt. 856 | 0x00007ffff72e7783 in __select_nocancel () from /lib64/libc.so.6 857 | Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64 libgcc-4.8.5-16.el7_4.1.x86_64 libstdc++-4.8.5-16.el7_4.1.x86_64 858 | (gdb) bt 859 | #0 0x00007ffff72e7783 in __select_nocancel () from /lib64/libc.so.6 860 | #1 0x0000000000400c75 in main (argc=1, argv=0x7fffffffe5f8) at select_client_tvnull.cpp:51 861 | (gdb) c 862 | Continuing. 863 | recv data: hello 864 | 865 | ^C 866 | Program received signal SIGINT, Interrupt. 867 | 0x00007ffff72e7783 in __select_nocancel () from /lib64/libc.so.6 868 | (gdb) c 869 | Continuing. 870 | recv data: world 871 | 872 | ``` 873 | 874 | 如上输出结果所示,我们使用 gdb 的 **r** 命令(run)将程序跑起来后,程序卡在某个地方,我们按 Ctrl + C 875 | 876 | (代码中的 **^C**)中断程序后使用 **bt** 命令查看当前程序的调用堆栈,发现确实阻塞在 **select** 函数调用处;接着我们在服务器端给客户端发送一个 **hello** 数据: 877 | 878 | ``` 879 | [root@myaliyun ~]# nc -v -l 0.0.0.0 3000 880 | Ncat: Version 6.40 ( http://nmap.org/ncat ) 881 | Ncat: Listening on 0.0.0.0:3000 882 | Ncat: Connection from 127.0.0.1. 883 | Ncat: Connection from 127.0.0.1:55968. 884 | hello 885 | ``` 886 | 887 | 客户端收到数据后,**select** 函数满足条件,立即返回,并将数据输出来后继续进行下一轮 **select** 检测,我们使用 Ctrl + C 将程序中断,发现程序又阻塞在 **select** 调用处;输入 **c** 命令(continue)让程序继续运行, 此时,我们再用服务器端给客户端发送 **world** 字符串,**select** 函数再次返回,并将数据打印出来,然后继续进入下一轮 select 检测,并继续在 select 处阻塞。 888 | 889 | ``` 890 | [root@myaliyun ~]# nc -v -l 0.0.0.0 3000 891 | Ncat: Version 6.40 ( http://nmap.org/ncat ) 892 | Ncat: Listening on 0.0.0.0:3000 893 | Ncat: Connection from 127.0.0.1. 894 | Ncat: Connection from 127.0.0.1:55968. 895 | hello 896 | world 897 | ``` 898 | 899 | 5. **在 Linux 平台上,select 函数的第一个参数必须设置成需要检测事件的所有 fd 中的最大值加 1**。所以上文中 **select_server.cpp** 中,每新产生一个 clientfd,我都会与当前最大的 **maxfd** 作比较,如果大于当前的 **maxfd** 则将 **maxfd** 更新成这个新的最大值。其最终目的是为了在 **select** 调用时作为第一个参数(加 1)传进去。 900 | 901 | > 在 Windows 平台上,select 函数的第一个值传任意值都可以,Windows 系统本身不使用这个值,只是为了兼容性而保留了这个参数,但是在实际开发中为了兼容跨平台代码,也会按惯例,将这个值设置为最大 socket 加 1。这点请读者注意。 902 | 903 | 904 | 905 | 以上是我总结的 Linux 下 select 使用的**五个注意事项**,希望读者能理解它们。 906 | 907 | Linux select 函数的缺点也是显而易见的: 908 | 909 | - 每次调用 **select** 函数,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 较多时会很大,同时每次调用 **select** 函数都需要在内核遍历传递进来的所有 fd,这个开销在 fd 较多时也很大; 910 | - 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义然后重新编译内核的方式提升这一限制,这样非常麻烦而且效率低下; 911 | - **select** 函数在每次调用之前都要对传入参数进行重新设定,这样做比较麻烦而且会降低性能。 912 | 913 | 914 | 915 | > 在 Linux 平台上,select 函数的实现是利用 poll 函数的,有兴趣的读者可以查找一下相关的资料来阅读一下。关于 poll 函数的使用,接下来我们会介绍。 916 | 917 | 918 | 919 | #### Windows 平台上 select 函数不会修改 timeval 的值 920 | 921 | 上文提到,在 Windows 系统上,**select** 函数结束后,不会修改其参数 **timeval** 的值。我们可以使用下面这段代码来验证: 922 | 923 | ``` 924 | bool Connect(const char* pServer, short nPort) 925 | { 926 | SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0); 927 | if (hSocket == INVALID_SOCKET) 928 | return false; 929 | 930 | //将socket设置成非阻塞的 931 | unsigned long on = 1; 932 | if (::ioctlsocket(hSocket, FIONBIO, &on) == SOCKET_ERROR) 933 | return false; 934 | 935 | struct sockaddr_in addrSrv = { 0 }; 936 | struct hostent* pHostent = NULL; 937 | unsigned int addr = 0; 938 | 939 | if ((addrSrv.sin_addr.s_addr = inet_addr(pServer) == INADDR_NONE) 940 | { 941 | pHostent = ::gethostbyname(pServer); 942 | if (!pHostent) 943 | return false; 944 | else 945 | addrSrv.sin_addr.s_addr = *((unsigned long*)pHostent->h_addr); 946 | } 947 | 948 | addrSrv.sin_family = AF_INET; 949 | addrSrv.sin_port = htons((nPort); 950 | int ret = ::connect(hSocket, (struct sockaddr*)&addrSrv, sizeof(addrSrv)); 951 | if (ret == 0) 952 | return true; 953 | 954 | if (ret == SOCKET_ERROR && WSAGetLastError() != WSAEWOULDBLOCK) 955 | return false; 956 | 957 | fd_set writeset; 958 | FD_ZERO(&writeset); 959 | FD_SET(hSocket, &writeset); 960 | struct timeval tm = { 3, 200 }; 961 | if (::select(hSocket + 1, NULL, &writeset, NULL, &tm) != 1) 962 | { 963 | printf("tm.tv_sec: %d, tm.tv_usec: %d\n", tm.tv_sec, tm.tv_usec); 964 | return false; 965 | } 966 | 967 | printf("tm.tv_sec: %d, tm.tv_usec: %d\n", tm.tv_sec, tm.tv_usec); 968 | 969 | return true; 970 | } 971 | ``` 972 | 973 | 上述代码中,**38** 行调用了 **select** 函数,无论 **select** 是成功还是出错,我们都会打印出其参数的 **tm** 的值(**40** 和 **44** 行),经测试验证 **tm** 结构体的两个成员值在 **select** 函数调用前后并没有发生改变。 974 | 975 | 976 | 977 | > 虽然 Windows 系统并不会改变 select 的超时时间参数的值,但是为了代码的跨平台性,我们在实际开发中不应该依赖这种特性,而是每次调用 select 函数前都重新给超时时间参数重新设置值。 978 | 979 | 980 | 981 | 982 | 983 | ------ 984 | 985 | **本文首发于『easyserverdev』公众号,欢迎关注,转载请保留版权信息。** 986 | 987 | **欢迎加入高性能服务器开发 QQ 群一起交流: 578019391 。** 988 | 989 | ![微信扫码关注](http://www.hootina.org/github_easyserverdev/articlelogo.jpg) --------------------------------------------------------------------------------