Windows 套接字入门

快速开始

使用 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 占用的资源。

1
WSACleanup()

实现 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_addrai_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_addrlenai_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";

pNodeNameNULL 并且 ppResult 包含 AI_PASSIVE 标志时,getaddrinfo() 会把 ai_addr 设置为 INADDR_ANYIN6ADDR_ANY_INIT,前者是全 0 的 IPv4 地址,后者是全 0 的 IPv6 地址。

创建套接字

使用 addrinfo 结构的 ai_familyai_socktypeai_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_addrai_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_onoff0 时,l_linger 被忽略,closesocket() 函数总是立即返回 0。若接收缓存还有数据,则连接会粗暴断开;否则,若发送缓存还有数据,则连接会在所有数据发送完后正常断开。
  • l_onoff 不为 0l_linger0 时,closesocket() 函数总是立即返回 0。连接总是立即粗暴断开,无论接收缓存和发送缓存是否为空。
  • l_onoff 不为 0l_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[])
{
// Initialize Winsock
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";


// Creating a Socket
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";


// Binding a Socket
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";


// Listening on a Socket
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";


// Accepting a connection
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";


// Receiving and Sending Data
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[])
{
// Initialize Winsock
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";


// Creating a Socket
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";


// Connecting to a Socket
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";


// Sending and Receiving Data
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;
}