C++

Winsock客户端和服务器代码

Posted by r3kind1e on October 12, 2021

Winsock 入门

原文:带 Winsock 的入门

下面是 Windows 套接字编程入门的循序渐进指南。 它旨在帮助您了解基本的 Winsock 函数和数据结构,以及它们如何一起工作。

用于说明的客户端和服务器应用程序是一个非常基本的客户端和服务器。 Microsoft Windows 软件开发工具包 (SDK) 附带的示例中提供了更高级的代码示例。

对于客户端和服务器应用程序,前几个步骤是相同的。

以下部分介绍了创建 Winsock 客户端应用程序的剩余步骤。

以下部分介绍了创建 Winsock 服务器应用程序的剩余步骤。

这些基本示例的完整源代码。

关于服务器和客户端

原文:关于服务器和客户端

套接字网络应用程序有两种不同类型:服务器和客户端。

服务器和客户端具有不同的行为;因此,创建它们的过程是不同的。 下面是用于创建流式 TCP/IP 服务器和客户端的常规模型。

服务器

  1. 初始化Winsock。
  2. 创建套接字。
  3. 绑定套接字。
  4. 在套接字上侦听客户端。
  5. 接受来自客户端的连接。
  6. 接收和发送数据。
  7. 断开连接。

客户端

  1. 初始化Winsock。
  2. 创建套接字。
  3. 连接到该服务器。
  4. 发送和接受数据。
  5. 断开连接。

对于客户端和服务器,某些步骤是相同的。 这些步骤的实现几乎完全相同。 本指南中的一些步骤特定于正在创建的应用程序类型。

创建基本 Winsock 应用程序

原文:创建基本Winsock应用程序

创建基本 Winsock 应用程序

  1. 创建新的空项目。
  2. 将空的 C++ 源文件添加到项目。
  3. 确保生成环境引用 Microsoft Windows 软件开发工具包 (SDK) 或早期平台软件开发工具包 (SDK) 的 Include、Lib 和 Src 目录。
  4. 确保生成环境链接到 Winsock 库文件 Ws2_32.lib。 使用 Winsock 的应用程序必须与 Ws2_32.lib库文件链接。 #pragma comment向链接器(linker)指示需要 Ws2_32.lib文件。
  5. 开始对 Winsock 应用程序进行编程。 通过包含 Winsock 2 头文件来使用 Winsock API。 Winsock2.h头文件包含大多数 Winsock 函数、结构和定义。 Ws2tcpip.h头文件包含 WINSock 2 Protocol-Specific TCP/IP 附录文档中引入的定义,其中包括用于检索 IP 地址的较新函数和结构。

Stdio.h 用于标准输入和输出,特别是 printf () 函数。

1
2
3
4
5
6
7
8
9
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

int main() {
  return 0;
}

如果应用程序使用 IP 帮助程序 API,则Iphlpapi.h头文件是必需的。 当需要Iphlpapi.h头文件时,Winsock2.h头文件的#include 行应放在Iphlpapi.h头文件的 #include 行之前。

Winsock2.h头文件在内部包含Windows.h头文件中的核心元素,因此 Winsock 应用程序中通常没有Windows.h头文件的#include行。 如果Windows.h头文件需要#include行,则前面应带有#define WIN32_LEAN_AND_MEAN宏。 出于历史原因 ,Windows.h标头默认包含Windows Sockets 1.1 Winsock.h头文件。 Winsock.h头文件中的声明与 Windows Sockets 2.0需要的Winsock2.h头文件中的声明冲突。 WIN32_LEAN_AND_MEAN宏可防止Winsock.h包含在Windows.h标头中。 下面显示了一个说明这一点的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <stdio.h>

#pragma comment(lib, "Ws2_32.lib")

int main() {
  return 0;
}

初始化Winsock

原文:初始化Winsock

调用 Winsock (的应用程序或 DLL) 进程必须在进行其他 Winsock 函数调用之前初始化Windows Sockets DLL 的使用。 这也确保系统支持 Winsock。

初始化Winsock

  1. 创建名为wsaData的WSADATA对象。
1
WSADATA wsaData;
  1. 调用WSAStartup,并返回其值作为整数并检查错误。
1
2
3
4
5
6
7
8
int iResult;

// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
if (iResult != 0) {
    printf("WSAStartup failed: %d\n", iResult);
    return 1;
}

调用 WSAStartup 函数以启动 WS2 _32.dll。

WSADATA结构包含有关套接字Windows的信息。 WSAStartup的 MAKEWORD(2, 2)参数在系统上请求 Winsock 版本 2.2,并设置传递的版本作为调用方可以使用的 Windows Sockets 支持的最高版本。

创建 Winsock 客户端应用程序

为客户端创建套接字

原文:为客户端创建套接字

初始化之后,必须实例化SOCKET(套接字)对象以供客户端使用。

创建套接字

  1. 声明一个包含 sockaddr结构的 addrinfo对象并初始化这些值。 对于此应用程序,未指定 Internet 地址系列,以便可以返回 IPv6 或 IPv4 地址。 应用程序请求套接字类型为 TCP 协议的流套接字。
1
2
3
4
5
6
7
8
struct addrinfo *result = NULL,
                *ptr = NULL,
                hints;

ZeroMemory( &hints, sizeof(hints) );
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
  1. 调用 getaddrinfo 函数,请求在命令行上传递的服务器名称的 IP 地址。 在此示例中,客户端将连接到的服务器上的 TCP 端口被DEFAULT_PORT定义为27015。 getaddrinfo 函数以检查是否有错误的整数的形式返回其值。
1
2
3
4
5
6
7
8
9
#define DEFAULT_PORT "27015"

// Resolve the server address and port
iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
    printf("getaddrinfo failed: %d\n", iResult);
    WSACleanup();
    return 1;
}
  1. 创建名为 ConnectSocket 的 SOCKET(套接字)对象。
1
SOCKET ConnectSocket = INVALID_SOCKET;
  1. 调用 socket 函数并将其值返回到 ConnectSocket 变量。 对于此应用程序,请使用与hints参数中指定的地址族(address family)、套接字类型(socket type)和协议匹配的 getaddrinfo调用返回的第一个 IP 地址。 在此示例中,使用套接字类型SOCK_STREAMIPPROTO_TCP协议指定了TCP流套接字。 地址族(address family)未指定 (AF_UNSPEC) ,因此返回的 IP 地址可能是服务器的 IPv6 地址或 IPv4 地址。

    如果客户端应用程序只想使用 IPv6 或 IPv4 进行连接,则需要在hints参数中将地址族(address family)设置为AF_INET6 For IPv6,或者是AF_INET for IPv4。

1
2
3
4
5
6
7
8
9
// Attempt to connect to the first address returned by
// the call to getaddrinfo
//尝试连接到调用 getaddrinfo 返回的第一个地址 
ptr=result;

// Create a SOCKET for connecting to server
// 创建用于连接到服务器的 SOCKET 
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, 
    ptr->ai_protocol);
  1. 检查是否存在错误,以确保套接字为有效套接字。
1
2
3
4
5
6
if (ConnectSocket == INVALID_SOCKET) {
    printf("Error at socket(): %ld\n", WSAGetLastError());
    freeaddrinfo(result);
    WSACleanup();
    return 1;
}

对于不同实现,可以更改传递到 socket(套接字) 函数的参数。

错误检测是成功网络代码的重要部分。 如果 socket(套接字)函数调用失败,它将返回INVALID_SOCKET。 前面代码中的 if 语句用于捕获在创建套接字时可能发生的任何错误。 WSAGetLastError 返回与上次发生的错误相关联的错误号。

根据应用程序的不同,可能需要更全面的错误检查。

例如,将 hints.ai_family 设置为 AF_UNSPEC 会导致连接调用失败。 如果发生这种情况,请改用特定的 IPv4 (AF_INET) 或 IPv6 (AF_INET6) 值。

WSACleanup 用于终止WS2_32 DLL的使用。

连接到套接字

原文:连接到套接字

若要使客户端能够连接到网络,它必须连接到服务器。

调用 connect 函数,将创建的套接字和 sockaddr 结构作为参数传递。 检查常规错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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
// 如果连接调用失败,应该真正尝试 getaddrinfo 返回的下一个地址
// But for this simple example we just free the resources
// returned by getaddrinfo and print an error message
// 但是对于这个简单的例子,我们只是释放 getaddrinfo 返回的资源并打印错误消息 

freeaddrinfo(result);

if (ConnectSocket == INVALID_SOCKET) {
    printf("Unable to connect to server!\n");
    WSACleanup();
    return 1;
}

getaddrinfo函数用于确定 sockaddr 结构中的值。 本示例使用 getaddrinfo 函数返回的第一个 IP 地址来指定 传递给连接的 sockaddr 结构。 如果 第 一个 IP 地址的连接调用失败,请尝试从 getaddrinfo 函数返回的链接列表中的下一个 addrinfo结构。

sockaddr 结构中指定的信息包括:

  • 客户端将尝试连接到的服务器 IP 地址。
  • 客户端将连接到的服务器的端口号。 当客户端调用 getaddrinfo 函数时,此端口被指定为端口 27015。

在客户端上发送和接受数据

原文:在客户端上发送和接受数据

以下代码演示建立连接后客户端使用的 sendrecv 函数。

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
#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);

sendrecv函数都分别返回发送或接收的字节数的整数值或错误。 每个函数也采用相同的参数:活动套接字、 字符缓冲区、要发送或接收的字节数以及使用的任何标志。

断开客户端连接

原文:断开客户端连接

客户端完成发送和接收数据后,客户端会断开与服务器的连接并关闭套接字。

断开和关闭套接字

  1. 客户端完成向服务器发送数据后,可以调用 shutdown 函数,指定SD_SEND以关闭套接字的发送端。 这允许服务器释放此套接字的一些资源。 客户端应用程序仍可接收套接字上的数据。
1
2
3
4
5
6
7
8
9
// 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);
    WSACleanup();
    return 1;
}
  1. 客户端应用程序收到数据后,将调用 closesocket 函数来关闭套接字。

    当客户端应用程序完成使用Windows Sockets DLL,将调用 WSACleanup函数以释放资源。

1
2
3
4
5
// cleanup
closesocket(ConnectSocket);
WSACleanup();

return 0;

完成Winsock客户端代码

下面是基本 Winsock TCP/IP 客户端应用程序的完整源代码。

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
125
#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>


// Need to link with Ws2_32.lib, Mswsock.lib, and Advapi32.lib
#pragma comment (lib, "Ws2_32.lib")
#pragma comment (lib, "Mswsock.lib")
#pragma comment (lib, "AdvApi32.lib")


#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

int __cdecl main(int argc, char **argv) 
{
    WSADATA wsaData;
    SOCKET ConnectSocket = INVALID_SOCKET;
    struct addrinfo *result = NULL,
                    *ptr = NULL,
                    hints;
    const char *sendbuf = "this is a test";
    char recvbuf[DEFAULT_BUFLEN];
    int iResult;
    int recvbuflen = DEFAULT_BUFLEN;
    
    // Validate the parameters
    if (argc != 2) {
        printf("usage: %s server-name\n", argv[0]);
        return 1;
    }

    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    }

    ZeroMemory( &hints, sizeof(hints) );
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;

    // Resolve the server address and port
    iResult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result);
    if ( iResult != 0 ) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Attempt to connect to an address until one succeeds
    for(ptr=result; ptr != NULL ;ptr=ptr->ai_next) {

        // Create a SOCKET for connecting to server
        ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, 
            ptr->ai_protocol);
        if (ConnectSocket == INVALID_SOCKET) {
            printf("socket failed with error: %ld\n", WSAGetLastError());
            WSACleanup();
            return 1;
        }

        // Connect to server.
        iResult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
        if (iResult == SOCKET_ERROR) {
            closesocket(ConnectSocket);
            ConnectSocket = INVALID_SOCKET;
            continue;
        }
        break;
    }

    freeaddrinfo(result);

    if (ConnectSocket == INVALID_SOCKET) {
        printf("Unable to connect to server!\n");
        WSACleanup();
        return 1;
    }

    // Send an initial buffer
    iResult = send( ConnectSocket, sendbuf, (int)strlen(sendbuf), 0 );
    if (iResult == SOCKET_ERROR) {
        printf("send failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }

    printf("Bytes Sent: %ld\n", iResult);

    // shutdown the connection since no more data will be sent
    iResult = shutdown(ConnectSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ConnectSocket);
        WSACleanup();
        return 1;
    }

    // Receive until the peer 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 with error: %d\n", WSAGetLastError());

    } while( iResult > 0 );

    // cleanup
    closesocket(ConnectSocket);
    WSACleanup();

    return 0;
}

创建 Winsock 服务器应用程序

为服务器创建套接字

原文:为服务器创建套接字

初始化之后,必须实例化套接字对象以供服务器使用。

为服务器创建套接字

  1. getaddrinfo函数用于确定 sockaddr结构中的值:
  • AF_INET用于指定 IPv4 地址族。
  • SOCK_STREAM 用于指定流套接字。
  • IPPROTO_TCP 用于指定 tcp 协议。
  • AI_PASSIVE 标志指示调用方打算在对bind函数的调用中使用返回的套接字地址结构。 如果设置了AI_PASSIVE 标志,并且 getaddrinfo函数的 nodename 参数为 NULL 指针,则套接字地址结构的 IP 地址部分将为IPv4地址设置为INADDR_ANY,或为 IPv6 地址设置为IN6ADDR_ANY_INIT
  • 27015是与客户端将连接到的服务器关联的端口号。

addrinfo结构由 getaddrinfo函数使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define DEFAULT_PORT "27015"

struct 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;

// Resolve the local address and port to be used by the server
// 解析服务器要使用的本地地址和端口
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
    printf("getaddrinfo failed: %d\n", iResult);
    WSACleanup();
    return 1;
}
  1. 创建名为ListenSocket的套接字对象,使服务器侦听客户端连接。

    1
    
    SOCKET ListenSocket = INVALID_SOCKET;
    
  2. 调用 socket 函数并将其值返回到 ListenSocket 变量。 对于此服务器应用程序,请使用与hints参数中指定的地址族、套接字类型和协议匹配的 getaddrinfo调用返回的第一个 IP 地址。 在此示例中,使用 IPv4 地址族、套接字类型SOCK_STREAM和IPPROTO_TCP协议请求了 ipv4 的 TCP 流套接字。 因此,需要为 ListenSocket 请求 IPv4 地址。

    如果服务器应用程序想要侦听 IPv6,则需要在hints参数中将地址族设置为AF_INET6。如果服务器要侦听 IPv6 和 IPv4,则必须创建两个侦听套接字,一个用于 IPv6,一个用于 IPv4。这两个套接字必须由应用程序单独处理。

    WindowsVista 和更高版本提供了创建单个 IPv6 套接字的功能,该套接字处于双堆栈模式下以在 IPv6 和 IPv4 上进行侦听。 有关此功能的详细信息,请参阅 双堆栈套接字

    1
    2
    3
    4
    
    // Create a SOCKET for the server to listen for client connections
    // 为服务器创建一个 SOCKET 来监听客户端连接
       
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    
  3. 检查是否存在错误,以确保套接字为有效套接字。

    1
    2
    3
    4
    5
    6
    
    if (ListenSocket == INVALID_SOCKET) {
        printf("Error at socket(): %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }
    

绑定套接字

原文:绑定套接字

若要使服务器接受客户端连接,它必须绑定到系统内的网络地址。 以下代码演示如何将已创建的套接字绑定到 IP 地址和端口。 客户端应用程序使用 IP 地址和端口连接到主机网络。

sockaddr结构保存有关地址系列、IP 地址和端口号的信息。

调用 bind函数,将从 getaddrinfo函数返回的已创建的套接字和 sockaddr结构作为参数传递。 检查常规错误。

1
2
3
4
5
6
7
8
9
// 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 函数为此地址信息分配的内存。

1
freeaddrinfo(result);

侦听套接字

原文:侦听套接字

将套接字绑定到系统上的 IP 地址和端口之后,服务器必须在该 IP 地址和端口上侦听传入的连接请求。

调用listen函数,作为参数传递已创建的套接字和 积压工作(backlog)的队列的最大长度,即要接受的挂起连接的队列的最大长度。 在此示例中, 积压工作(backlog )参数设置为 SOMAXCONN。 此值是一个特殊常量,指示此套接字的 Winsock 提供程序允许队列中的最大合理数目的挂起连接。 检查返回值是否存在常规错误。

1
2
3
4
5
6
if ( listen( ListenSocket, SOMAXCONN ) == SOCKET_ERROR ) {
    printf( "Listen failed with error: %ld\n", WSAGetLastError() );
    closesocket(ListenSocket);
    WSACleanup();
    return 1;
}

接受连接(Windows Sockets 2)

原文:接受连接

当套接字侦听连接后,程序必须处理该套接字上的连接请求。

接受套接字上的连接

  1. 创建名为 ClientSocket 的临时套接字对象,以接受来自客户端的连接。

    1
    
    SOCKET ClientSocket;
    
  2. 通常,服务器应用程序将被设计为侦听来自多个客户端的连接。 对于高性能服务器,通常使用多个线程来处理多个客户端连接。

    有几种不同的编程技术使用 Winsock,可用于侦听多个客户端连接。 一种编程方法是创建一个连续循环,该循环使用listen函数检查连接请求 (请参阅 在套接字上进行侦听) 。 如果出现连接请求,应用程序将调用 acceptAcceptExWSAAccept 函数,并将工作传递到另一个线程来处理请求。 还有其他几种编程技术。

    请注意,此基本示例非常简单,不使用多个线程。 该示例还只是侦听并只接受单个连接。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    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. 当客户端连接被接受后,服务器应用程序通常会将接受的客户端套接字传递 (上述示例代码中的 ClientSocket 变量) 到工作线程或 i/o 完成端口,并继续接受其他连接。 在此基本示例中,服务器将继续执行下一步。

    还有许多其他编程技术可用于侦听和接受多个连接。 其中包括使用 select 函数或 WSAPoll 函数。 Microsoft Windows 软件开发工具包 (SDK) 附带的高级 Winsock 示例说明了其中部分编程技术的示例。

    备注

    在 Unix 系统上,服务器的一种常见编程方法是让应用程序侦听连接。 当连接被接受时,父进程将调用 fork(分叉) 函数来创建一个新的子进程来处理客户端连接,从父级继承套接字。 Windows 上不支持这种编程方法,因为不支持fork(分叉) 函数。 这种方法通常也不适用于高性能服务器,因为创建新进程所需的资源比线程所需的资源要大得多。

在服务器上接收和发送数据

原文: 在服务器上接收和发送数据

以下代码演示服务器使用的recvsend函数。

接收和发送套接字上的数据

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
#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);

sendrecv函数都分别返回发送或接收的字节数的整数值或错误。 每个函数也采用相同的参数:活动套接字、 字符 缓冲区、要发送或接收的字节数以及使用的任何标志。

断开服务器的连接

服务器完成从客户端接收数据并将数据发送回客户端后,服务器会断开与客户端的连接并关闭套接字。

断开并关闭套接字

  1. 当服务器将数据发送到客户端时,可以调用shutdown函数以指定SD_SEND来关闭套接字的发送端。 这允许客户端释放此套接字的某些资源。 服务器应用程序仍然可以在套接字上接收数据。
1
2
3
4
5
6
7
8
9
// 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;
}
  1. 当客户端应用程序接收到数据后,将调用closesocket函数来关闭套接字。当使用Windows Sockets DLL完成客户端应用程序时,将调用 WSACleanup函数以释放资源。
1
2
3
4
5
// cleanup
closesocket(ClientSocket);
WSACleanup();

return 0;

完成Winsock服务器代码

下面是基本 Winsock TCP/IP 服务器应用程序的完整源代码。

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
125
126
127
128
129
130
131
132
133
134
135
136
137
#undef UNICODE

#define WIN32_LEAN_AND_MEAN

#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdlib.h>
#include <stdio.h>

// Need to link with Ws2_32.lib
#pragma comment (lib, "Ws2_32.lib")
// #pragma comment (lib, "Mswsock.lib")

#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"

int __cdecl main(void) 
{
    WSADATA wsaData;
    int iResult;

    SOCKET ListenSocket = INVALID_SOCKET;
    SOCKET ClientSocket = INVALID_SOCKET;

    struct addrinfo *result = NULL;
    struct addrinfo hints;

    int iSendResult;
    char recvbuf[DEFAULT_BUFLEN];
    int recvbuflen = DEFAULT_BUFLEN;
    
    // Initialize Winsock
    iResult = WSAStartup(MAKEWORD(2,2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed with error: %d\n", iResult);
        return 1;
    }

    ZeroMemory(&hints, sizeof(hints));
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags = AI_PASSIVE;

    // Resolve the server address and port
    iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
    if ( iResult != 0 ) {
        printf("getaddrinfo failed with error: %d\n", iResult);
        WSACleanup();
        return 1;
    }

    // Create a SOCKET for connecting to server
    ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
    if (ListenSocket == INVALID_SOCKET) {
        printf("socket failed with error: %ld\n", WSAGetLastError());
        freeaddrinfo(result);
        WSACleanup();
        return 1;
    }

    // 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;
    }

    freeaddrinfo(result);

    iResult = listen(ListenSocket, SOMAXCONN);
    if (iResult == SOCKET_ERROR) {
        printf("listen failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    // Accept a client socket
    ClientSocket = accept(ListenSocket, NULL, NULL);
    if (ClientSocket == INVALID_SOCKET) {
        printf("accept failed with error: %d\n", WSAGetLastError());
        closesocket(ListenSocket);
        WSACleanup();
        return 1;
    }

    // No longer need server socket
    closesocket(ListenSocket);

    // 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 with error: %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 with error: %d\n", WSAGetLastError());
            closesocket(ClientSocket);
            WSACleanup();
            return 1;
        }

    } while (iResult > 0);

    // shutdown the connection since we're done
    iResult = shutdown(ClientSocket, SD_SEND);
    if (iResult == SOCKET_ERROR) {
        printf("shutdown failed with error: %d\n", WSAGetLastError());
        closesocket(ClientSocket);
        WSACleanup();
        return 1;
    }

    // cleanup
    closesocket(ClientSocket);
    WSACleanup();

    return 0;
}

运行Winsock客户端和服务器代码示例

本部分包含 TCP/IP 客户端和服务器应用程序的完整源代码:

启动客户端应用程序之前应启动服务器应用程序。

若要执行服务器,请编译完整的服务器源代码并运行可执行文件。 服务器应用程序在 TCP 端口27015上侦听客户端连接。 当客户端连接后,服务器将从客户端接收数据,而回显 (将接收的数据) 发送回客户端。 当客户端关闭连接时,服务器将关闭客户端套接字、关闭socket并退出。

若要执行该客户端,请编译完整的客户端源代码并运行可执行文件。 客户端应用程序要求在执行客户端时将运行服务器应用程序的计算机的名称或 IP 地址作为命令行参数进行传递。 如果在示例计算机上执行客户端和服务器,则可以按如下所示启动客户端:

client localhost

客户端尝试连接到 TCP 端口27015上的服务器。 客户端连接后,客户端会将数据发送到服务器,并接收从服务器发送回的任何数据。 然后,客户端会关闭套接字并退出。

参考:VS设置同时启动多个项目