快速开始
使用 Winsock 的程序必须包含头文件 winsock2.h
并链接到动态库 Ws2_32.dll
。
1 2
| #include <winsock2.h> #include <ws2tcpip.h>
|
头文件 ws2tcpip.h
包含一些用于处理 IP 地址的函数,比如 getaddrinfo()
函数。
用到 IP Helper API 的程序还需要头文件 Iphlpapi.h
。在包含 Iphlpapi.h
时,要先包含 winsock2.h
。
Winsock2.h
已经包含了 windows.h
。如果需要先包含 windows.h
,则包含 windows.h
前要先定义宏 WIN32_LEAN_AND_MEAN
,例如:
1 2 3 4 5 6
| #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif
#include <windows.h> #include <winsock2.h>
|
初始化
在调用任何 Winsock 函数之前,要先调用 WSAStartup()
函数初始化 Winsock。
1 2 3 4 5 6 7
| WSADATA wsaData; int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { std::cerr << "WSAStartup() failed, error " << iResult << std::endl; return 1; } std::cout << "WSAStartup() succeeded\n";
|
WSADATA
结构用于保存 Winsock 的信息。MAKEWORD(2, 2)
表示需要 Winsock 2.2。
不需要再使用 Winsock 时,应该调用 WSACleanup()
函数释放 Winsock 占用的资源。
实现 TCP 服务器
套接字用 addrinfo
结构描述,它有下列成员:
ai_family
指示 IP 地址的类型,AF_INET
表示 IPv4,AF_INET6
表示 IPv6。
ai_socktype
指示服务的类型,SOCK_STREAM
表示字节流,SOCK_DGRAM
表示数据报。
ai_protocol
指示传输层协议,IPPROTO_TCP
表示 TCP 协议,IPPROTO_UDP
表示 UDP 协议。
ai_flags
指示套接字的其他属性,AI_PASSIVE
表示套接字用于被动接受客户端的连接。
ai_addr
包含 IP 地址和端口号。
ai_addrlen
指示 IP 地址和端口号的长度。
ai_next
指向下一个 addrinfo
结构,用于形成 addrinfo
结构的链表。
ai_addr
和 ai_addrlen
两个字段要借助 getaddrinfo()
函数设置。getaddrinfo()
函数可以将主机名或字符串表示的 IP 地址转换成 IP 地址,它的原型如下:
1 2 3 4 5 6
| INT WSAAPI getaddrinfo( [in, optional] PCSTR pNodeName, [in, optional] PCSTR pServiceName, [in, optional] const ADDRINFOA *pHints, [out] PADDRINFOA *ppResult );
|
pNodeName
主机名或字符串表示的 IP 地址,可以是 NULL
,如 "localhost"
或 "127.0.0.1"
。
pServiceName
服务名称或字符串表示的端口号,如 "http"
或 "80"
。
pHints
描述套接字的 addrinfo
结构。
ppResult
如果 getaddrinfo()
函数调用成功,它将指向一个 addrinfo
结构的链表。
getaddrinfo()
函数会根据 hints
创建一个 addrinfo
结构的链表,其中的每一个 addrinfo
结构的 ai_addrlen
和 ai_addr
两个字段均已设置。链表的内存空间由 getaddrinfo()
函数自动分配。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct addrinfo hints, result = NULL; ZeroMemory(&hints, sizeof (hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = AI_PASSIVE;
iResult = iResult = getaddrinfo("127.0.0.1", "80", &hints, &result); if (iResult != 0) { std::cerr << "getaddrinfo() failed, error " << iResult << std::endl; WSACleanup(); return 1; } std::cout << "getaddrinfo() succeeded\n";
|
当 pNodeName
是 NULL
并且 ppResult
包含 AI_PASSIVE
标志时,getaddrinfo()
会把 ai_addr
设置为 INADDR_ANY
或 IN6ADDR_ANY_INIT
,前者是全 0 的 IPv4 地址,后者是全 0 的 IPv6 地址。
创建套接字
使用 addrinfo
结构的 ai_family
、ai_socktype
和 ai_protocol
三个成员作为参数调用 socket()
函数即可创建一个套接字。套接字的唯一标识是它的描述符(Descriptor)。socket()
函数在套接字创建成功时返回一个套接字的描述符,在发生错误时返回非法的套接字描述符 INVALID_SOCKET
。
1 2 3 4 5 6 7 8
| SOCKET ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); if (ListenSocket == INVALID_SOCKET) { std::cerr << "socket() failed, error " << WSAGetLastError() << std::endl; freeaddrinfo(result); WSACleanup(); return 1; } std::cout << "socket() succeeded\n";
|
当 Winsock 函数发生错误时,可以调用 WSAGetLastError()
函数获取 错误代号。
绑定 IP 地址和端口号
使用新建的套接字的描述符以及 addrinfo
结构的 ai_addr
和 ai_addrlen
两个成员作为参数调用 bind()
函数即可将套接字与本机的 IP 地址和端口号绑定。
1 2 3 4 5 6 7 8 9 10
| iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen); if (iResult == SOCKET_ERROR) { std::cerr << "bind() failed, error " << WSAGetLastError() << std::endl; freeaddrinfo(result); closesocket(ListenSocket); WSACleanup(); return 1; } freeaddrinfo(result); std::cout << "bind() succeeded\n";
|
将套接字与地址绑定之后,就可以用 freeaddrinfo()
函数释放 getaddrinfo()
函数为 addrinfo
链表分配的内存空间了。
接受客户端连接
将套接字与 IP 地址和端口号绑定后,调用 listen()
函数即可开始接受客户端建立连接的请求。与客户端建立连接时,服务器需要创建新的套接字。每建立一个新的连接,服务器就需要创建一个新的套接字。随着连接的不断建立,新的套接字将形成一个队列。第二个参数 backlog
用于限制队列的长度,宏 SOMAXCONN
的值是系统所能承受的最大长度。
1 2 3 4 5 6 7 8
| iResult = listen(ListenSocket, SOMAXCONN); if (iResult == SOCKET_ERROR) { std::cerr << "listen() failed, error " << WSAGetLastError() << std::endl; closesocket(ListenSocket); WSACleanup(); return 1; } std::cout << "listen() succeeded\n";
|
调用 listen()
函数之后,服务器就已经侦听在指定端口,可以和客户端完成 TCP 协议的三次握手。
处理客户端连接
用于获取队列中的套接字的函数是 accept()
,它返回套接字的描述符。
1 2 3 4 5 6 7 8
| SOCKET ClientSocket = accept(ListenSocket, NULL, NULL); if (ClientSocket == INVALID_SOCKET) { std::cerr << "accept() failed, error " << WSAGetLastError() << std::endl; closesocket(ListenSocket); WSACleanup(); return 1; } std::cout << "accept() succeeded\n";
|
服务器通常都需要同时处理多个客户端连接。有多种办法可以让服务器具备同时处理多个连接的能力。最简单的办法是,由主线程循环调用 accept()
函数获取队列中的套接字,再将套接字交给其他线程处理。此外,还可以借助 select() 或 WSAPoll() 函数。
传输数据
从接收缓存读数据可以用 recv()
函数;向发送缓存写数据可以用 sent()
函数。它们接受相同的参数:第一个参数指示一个套接字;第二个参数指示一段内存空间;第三个参数指示上述内存空间的大小;第四个参数会影响它们的行为。它们的返回值有相同的含义:如果读写成功,返回实际读写的字节数;如果对端发送了 FIN
报文段,返回 0
;如果读写失败,返回 SOCKET_ERROR
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| char recvbuf[1024]; iResult = recv(ClientSocket, recvbuf, sizeof(recvbuf), 0); if (iResult > 0) { std::cout << "recv() succeeded, " << iResult << " bytes received\n"; } else if (iResult == 0) { std::cout << "recv() succeeded, connection closed\n"; } else { std::cerr << "recv() failed, error " << WSAGetLastError() << std::endl; }
char sendbuf[] = "hello, world!"; iResult = send(ClientSocket, sendbuf, strlen(sendbuf), 0); if (iResult > 0) { std::cout << "send() succeeded, " << iResult << " bytes sent\n"; } else if (iResult == 0) { std::cout << "send() succeeded, connection closed\n"; } else { std::cerr << "send() failed, error " << WSAGetLastError() << std::endl; }
|
recv()
函数不一定立即返回:若接收缓存有数据,则立即返回读取的字节数;否则,对于阻塞的套接字,recv()
函数将阻塞直到收到数据或对端发送了 FIN
报文段;对于非阻塞的套接字,recv()
函数将立即返回 SOCKET_ERROR
并产生代号为 WSAEWOULDBLOCK
的错误。
send()
函数总是立即返回。注意,写缓存成功不等于发送成功。
在收到对端发送的 RST
报文段后读写缓存会导致代号为 WSAECONNRESET
的错误,错误消息是 Connection reset by peer
。
断开连接
有两种断开连接的方式:
- Graceful disconnect,正常断开,将发送
FIN
报文段。
- Abortive disconnect,粗暴断开,将发送
RST
报文段。
如果发送缓存中还有数据,正常断开的情况下,这些数据会被发送,而粗暴断开的情况下,这些数据会被丢弃。正常断开连接的方式是使用 shutdown()
函数:
不需要再向对端发送数据时,用 shutdown()
函数关闭数据的发送方向。
1
| shutdown(ClientSocket, SD_SEND)
|
关闭发送方向后就不能再调用 sent()
函数写发送缓存,否则就会导致代号为 WSAESHUTDOWN
的错误。关闭发送方向时,本端会发送 FIN
报文段,使连接进入半关闭状态,即 FIN_WAIT1
状态。若关闭发送方向时发送缓存中还有数据,则 FIN
报文段会在所有数据发送完后再发送。
不希望再收到对端的数据时,用 shutdown()
函数关闭数据的接收方向。
1
| shutdown(ClientSocket, SD_RECEIVE)
|
关闭接收方向后就不能再调用 recv()
函数读接收缓存,否则就会导致代号为 WSAESHUTDOWN
的错误。如果接收缓存还有数据未读取就关闭接收方向,或者关闭接收方向后又收到对端的数据,本端就会发送 RST
报文段。
关闭套接字
不再使用的套接字要用 closesocket()
函数关闭,以释放套接字占用的资源:
1
| closesocket(ClientSocket);
|
若调用 closesocket()
函数时连接还未断开,则 closesocket()
函数会尝试断开连接。closesocket()
函数断开连接的方式由 linger
结构决定:
1 2 3 4
| typedef struct linger { u_short l_onoff; u_short l_linger; } LINGER, *PLINGER, *LPLINGER;
|
l_onoff
指示 closesocket()
函数在断开连接并关闭套接字之前是否需要先等待一段时间,默认值 0
表示不等待,非 0
值表示需要等待,等待时长由 l_linger
指示,单位秒。具体行为如下:
- 当
l_onoff
为 0
时,l_linger
被忽略,closesocket()
函数总是立即返回 0
。若接收缓存还有数据,则连接会粗暴断开;否则,若发送缓存还有数据,则连接会在所有数据发送完后正常断开。
- 当
l_onoff
不为 0
且 l_linger
为 0
时,closesocket()
函数总是立即返回 0
。连接总是立即粗暴断开,无论接收缓存和发送缓存是否为空。
- 当
l_onoff
不为 0
且 l_linger
也不为 0
时:
- 对于阻塞的套接字:若接收缓存还有数据,则连接会粗暴断开;否则,若发送缓存中的数据能够在超时之前发送完,则连接会正常断开;否则,连接会粗暴断开。无论连接以哪种方式断开,
closesocket()
函数总是阻塞直到连接断开并返回 0
。
- 对于非阻塞的套接字:若接收缓存还有数据,则连接会粗暴断开;否则,若发送缓存为空,则连接正常断开;否则,连接状态保持不变。无论连接是否断开,
closesocket()
函数总是立即返回。若连接断开,则 closesocket()
函数返回 0
;否则,closesocket()
函数返回 SOCKET_ERROR
并产生代号为 WSAEWOULDBLOCK
的错误。
linger
结构可以用 setsockopt()
函数设置:
1 2 3 4
| LINGER linger; linger.l_onoff = 1; linger.l_linger = 1; setsockopt(ClientSocket, SOL_SOCKET, SO_LINGER, (const char*)&linger, sizeof(linger));
|
套接字默认是阻塞的。可以用 ioctlsocket() 函数把套接字设置为非阻塞的:
1 2
| u_long ulMode = 1; ioctlsocket(ClientSocket, FIONBIO, &ulMode);
|
实现 TCP 客户端
客户端在用 socket()
函数创建套接字后,就可以用 connect()
函数和服务器建立连接了。
1 2 3 4 5 6 7 8 9 10
| iResult = connect(ConnectSocket, result->ai_addr, (int)result->ai_addrlen); if (iResult == SOCKET_ERROR) { std::cerr << "connect() failed with error " << WSAGetLastError() << std::endl; closesocket(ConnectSocket); freeaddrinfo(result); WSACleanup(); return 1; } freeaddrinfo(result); std::cout << "connect() succeeded\n";
|
参考文献
附录:完整的 TCP 服务器和客户端
客户端和服务器建立连接后:
- 客户端首先向服务器发送一些数据,接着关闭发送方向,然后从服务器接收数据,最后关闭接收方向并关闭套接字。
- 服务器首先从客户端接收数据,接着关闭接收方向,然后向客户端发送一些数据,最后关闭发送方向并关闭套接字。
server.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| #include <iostream> #include <winsock2.h> #include <ws2tcpip.h>
int main(int argc, char const *argv[]) { WSADATA wsaData; int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { std::cerr << "WSAStartup() failed, error " << iResult << std::endl; return 1; } std::cout << "WSAStartup() succeeded\n";
struct addrinfo hints, *result = NULL; ZeroMemory(&hints, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = AI_PASSIVE;
iResult = getaddrinfo("localhost", "23", &hints, &result); if (iResult != 0) { std::cerr << "getaddrinfo() failed, error " << iResult << std::endl; WSACleanup(); return 1; } std::cout << "getaddrinfo() succeeded\n";
SOCKET ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); if (ListenSocket == INVALID_SOCKET) { std::cerr << "socket() failed, error " << WSAGetLastError() << std::endl; freeaddrinfo(result); WSACleanup(); return 1; } std::cout << "socket() succeeded\n";
iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen); if (iResult == SOCKET_ERROR) { std::cerr << "bind() failed, error " << WSAGetLastError() << std::endl; closesocket(ListenSocket); freeaddrinfo(result); WSACleanup(); return 1; } freeaddrinfo(result); std::cout << "bind() succeeded\n";
iResult = listen(ListenSocket, SOMAXCONN); if (iResult == SOCKET_ERROR) { std::cerr << "listen() failed, error " << WSAGetLastError() << std::endl; closesocket(ListenSocket); WSACleanup(); return 1; } std::cout << "listen() succeeded\n";
SOCKET ClientSocket = accept(ListenSocket, NULL, NULL); if (ClientSocket == INVALID_SOCKET) { std::cerr << "accept() failed, error " << WSAGetLastError() << std::endl; closesocket(ListenSocket); WSACleanup(); return 1; } std::cout << "accept() succeeded\n";
do { char recvbuf[1024]; iResult = recv(ClientSocket, recvbuf, sizeof(recvbuf), 0); if (iResult > 0) { std::cout << "recv() succeeded, " << iResult << " bytes received\n"; } else if (iResult == 0) { std::cout << "recv() succeeded, connection closed\n"; break; } else { std::cerr << "recv() failed, error " << WSAGetLastError() << std::endl; break; } shutdown(ClientSocket, SD_RECEIVE);
char sendbuf[] = "server hello"; iResult = send(ClientSocket, sendbuf, strlen(sendbuf), 0); if (iResult > 0) { std::cout << "send() succeeded, " << iResult << " bytes sent\n"; } else if (iResult == 0) { std::cout << "send() succeeded, connection closed\n"; break; } else { std::cerr << "send() failed, error " << WSAGetLastError() << std::endl; break; } shutdown(ClientSocket, SD_SEND); } while (false);
closesocket(ClientSocket); closesocket(ListenSocket); WSACleanup(); return iResult; }
|
client.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
| #include <iostream> #include <winsock2.h> #include <ws2tcpip.h>
int main(int argc, char const *argv[]) { WSADATA wsaData; int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); if (iResult != 0) { std::cerr << "WSAStartup() failed with error " << iResult << std::endl; return 1; } std::cout << "WSAStartup() succeeded\n";
struct addrinfo hints, *result = NULL; ZeroMemory(&hints, sizeof(hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP;
iResult = getaddrinfo("localhost", "23", &hints, &result); if (iResult != 0) { std::cerr << "getaddrinfo() failed with error " << iResult << std::endl; WSACleanup(); return 1; } std::cout << "getaddrinfo() succeeded\n";
SOCKET ConnectSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); if (ConnectSocket == INVALID_SOCKET) { std::cerr << "socket() failed with error " << WSAGetLastError() << std::endl; freeaddrinfo(result); WSACleanup(); return 1; } std::cout << "socket() succeeded\n";
iResult = connect(ConnectSocket, result->ai_addr, (int)result->ai_addrlen); if (iResult == SOCKET_ERROR) { std::cerr << "connect() failed with error " << WSAGetLastError() << std::endl; closesocket(ConnectSocket); freeaddrinfo(result); WSACleanup(); return 1; } freeaddrinfo(result); std::cout << "connect() succeeded\n";
do { char sendbuf[] = "client hello"; iResult = send(ConnectSocket, sendbuf, strlen(sendbuf), 0); if (iResult > 0) { std::cout << "send() succeeded, " << iResult << " bytes sent\n"; } else if (iResult == 0) { std::cout << "send() succeeded, connection closed\n"; break; } else { std::cerr << "send() failed with error " << WSAGetLastError() << std::endl; break; } shutdown(ConnectSocket, SD_SEND);
char recvbuf[1024]; iResult = recv(ConnectSocket, recvbuf, sizeof(recvbuf), 0); if (iResult > 0) { std::cout << "recv() succeeded, " << iResult << " bytes received\n"; } else if (iResult == 0) { std::cout << "recv() succeeded, connection closed\n"; break; } else { std::cerr << "recv() failed with error " << WSAGetLastError() << std::endl; break; } shutdown(ConnectSocket, SD_RECEIVE); } while (false);
closesocket(ConnectSocket); WSACleanup(); return iResult; }
|