0%

Windows下的C++ Socket编程

Socket(套接字)

套接字是计算机进程与网络之间的一个接口,准确地说,是应用层与运输层之间的接口,因此套接字也成为应用程序与网络之间的API。一个套接字是连接的一个端点,客户端与服务器端通过套接字传输数据。每个套接字由一个IP地址和端口号决定,表示为“地址:端口”。

TCP协议

本文将采用TCP协议。TCP是一个面向连接的传输层协议,每个连接由客户端和服务器端的两个套接字地址唯一决定。这要求客户和服务器在开始能够互相传输数据前,先进行三次握手,才能够创建连接。流程上讲,客户端套接字先与服务器上负责“迎接”的套接字进行接触,然后服务器端新生成一个连接套接字,与客户端套接字建立连接。

流程

以下是创建流式TCP/IP客户端-服务器端的通用流程。

服务器端

  1. Initialize Winsock.
  2. Create a socket.
  3. Bind the socket.
  4. Listen on the socket for a client.
  5. Accept a connection from a client.
  6. Receive and send data.
  7. Disconnect.

客户端

  1. Initialize Winsock.
  2. Create a socket.
  3. Connect to the server.
  4. Send and receive data.
  5. Disconnect.

Socket初始化

以下是Windows Socket的初始化过程。
预处理部分:

1
2
3
4
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")

这是Windows Socket的API,其中stdio.h是用来引入标准输入输出函数的,例如printf()。第4行#pragma注释向链接器指示需要Ws2_32.lib文件。

初始化部分:

1
2
3
4
5
6
7
8
WSADATA wsaData;
int iResult = 0;
//指定Winsock版本,初始化Winsock
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}

客户端-服务器端信息收发程序

下面将基于一个简单的信息收发程序,简单地介绍客户端与服务器端的创建步骤。基本上基于上文中的通用流程。

客户端

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
#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")
constexpr auto DEFAULT_PORT = "27015"; //默认的服务器端端口号
constexpr auto DEFAULT_BUFLEN = 512; //接收消息的缓冲大小
int main(int argc, char** argv)
{
//初始化Winsock
WSADATA wsaData;
int iResult = 0;
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}
//-----------------------
//获取服务器地址信息
addrinfo* result = NULL, * ptr = NULL, hints; //储存地址信息的结构
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC; //不指定,可以同时兼容IPv6,IPv4
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP; //指定TCP协议
//指定服务器的地址和端口号
iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed: %d\n", iResult);
WSACleanup();
return 1;
}
//-----------------------
//创建连接Socket
SOCKET ConnectSocket = INVALID_SOCKET;
//getaddrinfo返回的是一个list,对其中每一个尝试连接直到成功
for (ptr = result; ptr; ptr = ptr->ai_next) {
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (ConnectSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
continue;
}
//给定服务器端地址,建立与服务器的连接
//其中客户端的IP地址与端口号会自动获取
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
}
else break;
}

freeaddrinfo(result);
//多次尝试后仍未成功连接
if (ConnectSocket == INVALID_SOCKET) {
printf("Unable to connect to server!\n");
WSACleanup();
return 1;
}
//-----------------------
//发送接收消息
char sendbuf[DEFAULT_BUFLEN];
scanf_s("%s", sendbuf);
char recvbuf[DEFAULT_BUFLEN];
//发送初始信息,返回值是发送的字节数
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
printf("Bytes Sent: %ld\n", iResult);
//不再需要发送消息了,可以关闭发送的通道
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
do {
iResult = recv(ConnectSocket, recvbuf, DEFAULT_BUFLEN, 0);
if (iResult > 0)
printf("Bytes received: %d\n", iResult);
else if (iResult == 0)
printf("Connection closed\n");
else
printf("recv failed: %d\n", WSAGetLastError());
} while (iResult > 0);

//-----------------------
//释放
closesocket(ConnectSocket);
WSACleanup();

return 0;
}

服务器端

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
114
115
116
117
118
119
120
121
122
123
124
#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32.lib")

constexpr auto DEFAULT_PORT = "27098";
constexpr auto DEFAULT_BUFLEN = 512;
int main(int argc, char** argv)
{
//初始化Winsock
WSADATA wsaData;
int iResult = 0;
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}
//-----------------------
//储存地址信息的结构
addrinfo* result = NULL, * ptr = NULL, hints;
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
hints.ai_flags = AI_PASSIVE; //声明该socket需要被bind
//如果第一个参数为NULL,则返回本机上的地址
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed: %d\n", iResult);
WSACleanup();
return 1;
}
//-----------------------
//建立ListenSocket
SOCKET ListenSocket = INVALID_SOCKET;
ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ListenSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
//将ListenSocket绑定到本机—即服务器上的地址
//sockaddr_in service;
//service.sin_family = AF_INET;
//service.sin_addr.s_addr = ADDR_ANY;
//service.sin_port = htons(27015);
//iResult = bind(ListenSocket, (SOCKADDR*)& service, sizeof(service));
iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("bind failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ListenSocket);
WSACleanup();
return 1;
}
freeaddrinfo(result);
//指示该socket为服务器的监听套接字
iResult = listen(ListenSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
printf("Listen failed with error: %ld\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
//-----------------------
//接受客户端连接
SOCKET ClientSocket;
//accept会一直等待直到客户连接
ClientSocket = accept(ListenSocket, NULL, NULL);
if (ClientSocket == INVALID_SOCKET) {
printf("accept failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
//通常,服务器在接收到客户端连接之后,会把这个连接放到一个单独的线程中,但此处略去
//-----------------------
//收发消息
char recvbuf[DEFAULT_BUFLEN];
int iSendResult;
int recvbuflen = DEFAULT_BUFLEN;

//消息接收循环
do {
//由于收到的消息不带有字符串终止符,因此每次都要将缓冲置全0,不然无法判断字符串终止
memset(recvbuf, 0, sizeof(recvbuf));
//recv将阻塞等待客户端消息,客户端将发送通道关闭后,recv返回0
iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
if (iResult > 0) {
printf("%s\n", recvbuf);
// Echo the buffer back to the sender
iSendResult = send(ClientSocket, recvbuf, iResult, 0);
if (iSendResult == SOCKET_ERROR) {
printf("send failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
printf("Bytes sent: %d\n", iSendResult);
}
else if (iResult == 0)
printf("Connection closing...\n");
else {
printf("recv failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
} while (iResult > 0);
//-----------------------
//释放
//服务器关闭
iResult = shutdown(ClientSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
closesocket(ClientSocket);
WSACleanup();
return 0;
}

要点

  • recv函数是一直阻塞的,直到对方发送消息,或者对方关闭send通道,因此连接结束后要及时释放send通道,否则一方将会一直在等待recv。
  • 运行时,在参数列表中输入“localhost”代表服务器为本机。
  • 在实际情况中,服务器端收到客户端连接请求后,会创建一个新的线程来处理该连接。
  • 在socket编程中,每一步都要注意错误检测。
  • 如果服务器要同时监听IPv4和IPv6,必须创建两个监听套接字,分别监听IPv4和IPv6。

一个大坑

一开始我客户端怎么都传不了信息到服务器端上,最后发现是端口不知道被什么占用了,修改默认的端口号即可。
怎么看端口占用呢?
首先进入cmd,输入“netstat -ano | findstr “:27015””,回车,其中27015是想要查询的端口号

发现27015端口不知道被什么进程给占用了,并且一直在监听,其中最右边的14020是进程的ID号。
进入任务管理器->详细信息,看到第二列“PID”,即为进程号,按进程号即可找到占用端口的进程。
找到罪魁祸首了,盘他!

测试

大成功!

参考资料: https://docs.microsoft.com/zh-cn/windows/win32/winsock/getting-started-with-winsock