Windows Socket编程
可参考:https://docs.microsoft.com/zh-cn/windows/win32/winsock/getting-started-with-winsock
0. 准备工作
#include<WinSock2.h>//包含头文件
#pragma comment(lib,"ws2_32.lib")// 链接库文件
1. 初始化工作
1.1 WSADATA结构体
参考:https://docs.microsoft.com/en-us/windows/win32/api/winsock/ns-winsock-wsadata
// 初始化
WSADATA wsadata;
WSADATA
是一个结构体类型的数据结构,包含了Windows Sockets执行所需的信息
1.2 WSAStartup函数
- 语法
int WSAStartup(
WORD wVersionRequeted,
LPWSADATA lpWSAData
);
-
参数
-
wVersionRequested
[in]调用者可以使用的最高版本的Windows套接字支持。高位字节指定次要版本(修订)号;低位字节指定主版本号。
这个参数有时会传入
MAKEWORD(2,2)
表示对系统上Winsock版本2.2的请求还可以指定WINSOCK_VERSION作为此参数的值。WINSOCK_VERSION是在winsock2.h中定义的宏。
-
lpWSAData
[out]指向WSADATA结构体的指针,这个结构体会用于接收WINdows Sockets执行的细节信息
-
-
返回值
如果没有错误出现,那么会返回0。如果出错了,那么会返回错误码
注意,当WSAStartup失败后,应用程序不能调用WSAGetLastError来确定错误码,因为这种情况下程序不会加载ws2.dll,不会创建存放错误信息的空间。
-
注意
可以调用多次,但必须为每一次调用在结束后使用WSACleanup函数
2. 客户端
2.1 创建套接字
主要内容
初始化之后,必须实例化套接字对象以供客户端使用。
创建套接字
-
声明一个包含sockaddr结构的addrinfo对象并初始化这些值。
关于sockaddr结构体,参考https://docs.microsoft.com/en-us/windows/win32/api/ws2def/ns-ws2def-addrinfoa
struct addrinfo *result = NULL, *ptr = NULL, hints; ZeroMemory( &hints, sizeof(hints) );//以0填充结构体 hints.ai_family = AF_UNSPEC;//未指定AddrFamily类型 hints.ai_socktype = SOCK_STREAM;//流式socket /* 流式socket提供序列化的、可靠的、双向的、基于连接的字节流。使用TCP协议 */ hints.ai_protocol = IPPROTO_TCP;//协议类型
-
调用getaddrinfo函数,从命令行获得服务器信息。
关于getaddrinfo函数,参考:https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo
-
创建变量
SOCKET ConnectSocket = INVALID_SOCKET;
-
给变量赋值
ConnectSocket = socket(ai_family, ai_socktype, ai_protocaol); // 错误处理 if (ConnectSocket == INVALID_SOCKET) { printf("Error at socket(): %ld\n", WSAGetLastError()); freeaddrinfo(result); WSACleanup(); return 1; }
2.2 连接到套接字
要使客户端在网络上通信,必须连接到服务器
连接到套接字
调用connect函数,将创建的套接字和sockaddr结构作为参数传递。检查一般错误。
// Connect to server.
iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
}
// Should really try the next address returned by getaddrinfo
// if the connect call failed
// But for this simple example we just free the resources
// returned by getaddrinfo and print an error message
freeaddrinfo(result);
if (ConnectSocket == INVALID_SOCKET) {
printf("Unable to connect to server!\n");
WSACleanup();
return 1;
}
// 关于connect函数语法
int WSAAPI connect(
SOCKET s,
const sockaddr *name,
int namelen//socketaddr字节长度
);// 返回值:没有错误返回0,否则返回错误码
2.3 在客户端上发送和接受数据
#define DEFAULT_BUFLEN 512
int recvbuflen = DEFAULT_BUFLEN;
const char *sendbuf = "this is a test";
char recvbuf[DEFAULT_BUFLEN];
int iResult;
// Send an initial buffer
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);
// shutdown the connection for sending since no more data will be sent
// the client can still use the ConnectSocket for receiving data
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
// Receive data until the server closes the connection
do {
iResult = recv(ConnectSocket, recvbuf, recvbuflen, 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);
关于send函数
send函数用于借助一个已连接的套接字发送数据
int WSAAPI send(
SOCKET s,
const char *buf,// 指向包含将要传输数据的缓冲区指针
int len,// 缓冲区中要传送的数据长度,以字节为单位
int flags//一般指定为0
);
// 返回值
// 如果没有错误发生,那么返回总共发送的字节数,否则返回错误码
关于recv函数
从一个已经连接的套接字或者一个绑定的但是无连接的套接字接受数据
int recv(
SOCKET s,
char *buf,
int len,
int flags
);
//If no error occurs, recv returns the number of bytes received and the buffer pointed to by the buf parameter will contain this data received. If the connection has been gracefully closed, the return value is zero.
2.4 断开客户端连接
完成客户端发送和接收数据的连接后,客户端会断开与服务器的连接,并关闭套接字。
断开并关闭套接字
- 当客户端将数据发送到服务器时,可以调用shutdown函数以指定SD_SEND来关闭套接字的发送端。这允许服务器释放此套接字的某些资源。 客户端应用程序仍可以在套接字上接收数据。
// shutdown the send half of the connection since no more data will be sent
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
/*
closesocket关闭一个已经存在的套接字
*/
WSACleanup();
return 1;
}
- 使用 Windows 套接字 DLL 完成客户端应用程序时,将调用 WSACleanup 函数来释放资源。
3. 服务器端
3.1 为服务器创建套接字
可参考:https://docs.microsoft.com/en-us/windows/win32/winsock/creating-a-socket-for-the-server
初始化一个socket对象之后,需要服务器端实例化
在服务器上创建一个套接字
- 需要指定ai_flags为
AI_PASSIVE
- 创建一个SOCKET对象,叫做ListenSocket用于监听客户端连接
- 使用SOCKET函数赋值
- 进行错误检查处理
3.2 绑定套接字
服务器绑定一个套接字,该套接字已经和一个ip地址和端口关联了起来。用户端使用这个IP地址和端口与服务器建立连接
绑定套接字
sockaddr结构体存放着address family、IP地址和端口
调用bing函数,并且传入已经创建好的socket和sockaddr结构体
// Setup the TCP listening socket
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;
}
一旦bind函数被调用了,由getaddrinfo返回的地址信息便不再被需要了。freeaddrinfo函数用于释放被getaddrinfo申请占用的内存空间
3.3 通过socket进行监听
在绑定IP地址和端口到到socket对象之后,服务器应该通过IP地址和端口监听即将到来的连接请求。
监听socket
调用listen函数,传入已经创建好的socket对象以及backlog(等待接受连接队列最大长度),backlog一般设置为SOMAXCONN。
if ( listen( ListenSocket, SOMAXCONN ) == SOCKET_ERROR ) {
printf( "Listen failed with error: %ld\n", WSAGetLastError() );
closesocket(ListenSocket);
WSACleanup();
return 1;
}
3.4 接受一个连接
一旦socket正在监听等待一个连接,程序必须处理通过socket建立的连接
接受一个基于socket的连接
-
创建一个暂时的SOCKET对象ClientSocket,用来从客户端接收连接
SOCKET ClientSocket;
-
一般来说,一个服务器应用会从多用户端监听连接。对于高性能服务器,一般使用多线程技术来处理多用户连接
有几种不同的编程技术来实现监听来自许多用户的连接。比如创建一个连续的循环,循环体中用到了listen这个函数。如果一个连接请求出现,那么程序会调用accept,AcceptEx,或者WSAAccept函数,然后将剩下的工作交给另一个线程处理。
以下展示的仅仅是一个监听单用户连接的例子
ClientSocket = INVALID_SOCKET;
// Accept a client socket
ClientSocket = accept(ListenSocket, NULL, NULL);
if (ClientSocket == INVALID_SOCKET) {
printf("accept failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
3.5 数据传输
#define DEFAULT_BUFLEN 512
char recvbuf[DEFAULT_BUFLEN];
int iResult, iSendResult;
int recvbuflen = DEFAULT_BUFLEN;
// Receive until the peer shuts down the connection
do {
iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
// 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);
3.6 关闭连接
-
服务器端从用户端完成接收数据的任务之后,可以指定参数SD_SEND给shutdown函数然后调用它。这样可以让客户端释放socket资源,而服务器端仍然可以通过socket接收数据
// shutdown the send half of the connection since no more data will be sent iResult = shutdown(ClientSocket, SD_SEND); if (iResult == SOCKET_ERROR) { printf("shutdown failed: %d\n", WSAGetLastError()); closesocket(ClientSocket); WSACleanup(); return 1; }
-
当用户端应用完成接收数据的任务后,调用closesocket函数来关闭socket。
如果用户端不再使用Windows Sockets DLL之后,要调用WSACleanup()函数来释放资源