02-简单的C/S阻塞模型

发布时间 2023-12-01 22:06:26作者: 西兰花战士

C/S阻塞模型是指客户端/服务器阻塞模型,它描述了一种基于阻塞的网络通信方式。在阻塞模型中,客户端发送请求给服务器,并等待服务器的响应。在等待服务器响应的过程中,客户端的操作会被阻塞,直到服务器响应返回或超时。

服务器

服务器基本流程如下:

  1. 启动网络库
  2. 创建服务器Socket
  3. 绑定服务器地址和端口号
  4. 进入监听模式
  5. 接收客户端连接请求
  6. 与客户端进行通信
  7. 退出,清理工作和关闭网络库

创建套接字-socket函数

通过调用socket()函数,操作系统在内核种创建一个网络内核资源,并通过返回一个SOCKET类型的标识符唯一标识该内核对象。在后续的接口中,通过传入该标识符,操作系统能检索到对应的网络内核对象,从而完成相应操作。实际上,程序员只需要知道这个标识符代表一个网络服务即可,其他的不用关心。

该函数定义如下:

SOCKET socket(int af, int type, int protocol);

参数

  • af:地址类型,下面列出常用的几种地址类型。

 

  • type:套接字类型,下面列出常用的几种套接字类型。

 

  • protocol:协议类型,下面列出常用的几种套接字类型。

返回值

如果该函数调用成功,则返回一个可用的SocketID,如果函数调用失败,则返回INVALID_SOCKET,可用使用WSAGetLasterror()函数获取错误码。

绑定IP地址和端口号-bind()函数

调用socket()函数创建套接字后,需要将本地IP地址、端口号与SocketID进行绑定。调用bind函数绑定IP地址和端口号,可以使应用进程提供的网络服务与指定IP地址和端口号建立一对一的联系。这样,其他计算机可以通过指定的IP地址和端口号与该网络服务进行通信。bind函数定义如下:

int bind(SOCKET s, const struct sockaddr* addr, int namelen);

参数

  • s:合法的套接字ID。
  • addr:包含IP地址和端口号信息的结构体指针。
  • namelen:addr指向的指的大小(以字节为单位)。

返回值

如果未发生错误,该函数返回0。否则,该函数返回SOCKET_ERROR,可以通过WSAGetLasterror()函数获取错误码。

sockaddr结构体

1 struct sockaddr {
2     u_short sa_family;
3     char sa_data[14];
4 };

看到上面的成员一脸懵,实际上我们并不使用这个结构体,而是使用sockaddr_in结构体:

1 struct sockaddr_in {
2     short sin_family;      //地址类型,同socket()函数第一个参数
3     USHORT sin_port;       //端口号
4     IN_ADDR sin_addr;      //IP地址
5     CHAR sin_zero[8];      //占位符,预留给系统使用
6 };

该结构体与sockaddr结构体大小一致,在使用时只需将其强转为sockaddr类型即可。

IN_ADDR也是难懂的结构体,该结构体定义如下:

 1 //192.168.1.120
 2 struct in_addr {
 3     union {
 4         struct {
 5             UCHAR s_b1;            //192
 6             UCHAR s_b2;            //168
 7             UCHAR s_b3;            //1
 8             UCHAR s_b4;            //120
 9         } S_un_b;
10 
11         struct {
12             USHORT s_w1;        //高8位=168,低8位=192
13             USHORT s_w2;        //高8位=120,低8位=1
14         } S_un_w;
15 
16         ULONG S_addr;           //可以用inet_addr函数构造地址
17     } S_un;
18 
19 #define s_addr  S_un.S_addr /* can be used for most tcp & ip code */
20 #define s_host  S_un.S_un_b.s_b2    // host on imp
21 #define s_net   S_un.S_un_b.s_b1    // network
22 #define s_imp   S_un.S_un_w.s_w2    // imp
23 #define s_impno S_un.S_un_b.s_b4    // imp #
24 #define s_lh    S_un.S_un_b.s_b3    // logical host
25 };

使用方法如下:

1 {
2     sockaddr_in si;
3     si.sin_family = AF_INET;
4     si.sin_addr.s_addr = inet_addr("127.0.0.1");
5     si.sin_port = htons(12345);
6     bind(socketServer, reinterpret_cast<sockaddr*>(&si), sizeof(si));
7 }

进入监听状态-listen()函数

通过调用listen()函数,可以使当前的套接字进入监听状态,并能够接收客户端连接请求。

listen()函数定义如下:

int listen(SOCKET s, int backlog);

参数

  • s:待监听的套接字,通常是一个已经绑定IP地址和端口号的套接字。
  • backlog:用于指定连接请求队列的最大长度。当有客户端连接请求到达时,先将该请求放入请求队列中。一般填入SOMAXCONN,表示由系统选择合适的个数。

返回值

如果未发生错误,该函数返回0。否则,该函数返回SOCKET_ERROR,可以通过WSAGetLasterror()函数获取错误码。

接受客户端连接请求-accept()函数

当服务器进入监听状态后,如果此时有客户端连接,该连接将保存到请求队列中。而accept函数则从该队列中获取一个连接请求,通过函数返回值返回该请求的套接字。服务器可以使用这个新的套接字与相应的客户端进行数据交换。

accept函数定义如下:

SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);

参数

  • s:服务端正在监听的套接字。
  • addr:可选参数,指向 sockaddr 结构的指针,用于存储客户端地址信息。如果不需要获取该消息,可以传NULL。
  • namelen:addr指向的值的大小(以字节为单位)。

返回值

如果函数执行成功,它将返回一个新的套接字,该套接字代表服务器与客户端已经建立了一条连接,后续的交互可以通过该套接字完成。如果函数执行失败,则返回INVALID_SOCKET,可以通过WSAGetLasterror()函数获取错误码。

需要注意的是,如果 当前没有客户建立连接,则该函数将会阻塞,直到有客户端建立连接。

接收数据-recv()函数

recv()函数用于从指定的套接字中接收数据,该函数定义如下:

int recv(SOCKET s, char *buf, int len, int flags);

参数

  • s:从s套接字中读取数据。
  • buf:指向接收数据的缓冲区指针。
  • len:想要接收数据的最大长度。
  • flags:数据的读取方式。有如下几种取值:

前面5个标志可以单独使用,也可以使用按位或"|"组合使用。

返回值

如果函数执行成功,返回值表示接收到的数据的字节数。如果返回0,表示该Socket连接被断开。如果返回值为SOCKET_ERROR,表示发生错误,可以通过WSAGetLasterror()函数获取错误码。

发送数据-send()函数

send()函数用于向指定的套接字发送数据,该函数定义如下:

int send(SOCKET s, const char* buf, int len, int flags);

参数

  • s:向s套接字发送数据。
  • buf:指向待发送数据的缓冲区指针。
  • len:想要发送的数据长度
  • flags:数据的读取方式。有如下几种取值:

 

如果发送的数据量很大,超过了底层套接字缓冲区大小,send()函数可能会阻塞等待缓冲区有足够空间来容纳整个数据。如果需要确保所有数据都能成功发送,可以循环调用send函数,直到数据全部发送完成。

对于TCP协议,send()函数会保证数据的可靠传输,即使发生多次调用,数据会按照发送顺序传递给接收端。而对于UDP协议,send()函数并不保证数据的可靠传输,因此需要程序员自己实现可靠性验证和重传机制。

返回值

  • 如果send()函数成功发送了所有数据,返回值是发送的字节数。
  • 返回值大于0并且小于buf参数的长度,则表示部分数据被发送。
  • 返回值为0,表示连接被断开(客户端、服务器断开连接)。
  • 如果返回值为SOCKET_ERROR,表示发生错误,可以通过WSAGetLasterror()函数获取错误码。

关闭套接字-closesocket()函数

当不在使用socket套接字时,需要调用closesocket()函数手动释放套接字资源,该函数定义如下:

int closesocket (SOCKET s);

参数

  • s:合法的套接字ID。

返回值

如果未发生错误,该函数返回0。否则,该函数返回SOCKET_ERROR,可以通过WSAGetLasterror()函数获取错误码。

简单示例

  1 #define _WINSOCK_DEPRECATED_NO_WARNINGS
  2 
  3 #include <iostream>
  4 #include <WinSock2.h>
  5 #pragma comment(lib,"ws2_32.lib")
  6 using namespace std;
  7 
  8 const int nMajorVersion = 2;
  9 const int nMinorVersion = 2;
 10 
 11 int main()
 12 {
 13     WORD dwVersion = MAKEWORD(nMajorVersion, nMinorVersion);
 14     WSADATA data;
 15     int nRet = WSAStartup(dwVersion, &data);
 16     if (nRet != 0)
 17     {
 18         cout << "start network libary error!" << endl;
 19         return 0;
 20     }
 21 
 22     if (nMajorVersion != LOBYTE(data.wVersion) || nMinorVersion != HIBYTE(data.wVersion))
 23     {
 24         cout << "version error" << endl;
 25         WSACleanup();
 26         return 0;
 27     }
 28 
 29     SOCKET socketServer = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 30     if (INVALID_SOCKET == socketServer)
 31     {
 32         cout << "create socket error, error code = %d" << WSAGetLastError() << endl;
 33         WSACleanup();
 34         return 0;
 35     }
 36 
 37     sockaddr_in si;
 38     si.sin_family = AF_INET;
 39     si.sin_addr.s_addr = inet_addr("127.0.0.1");
 40     si.sin_port = htons(12345);
 41     nRet = bind(socketServer, reinterpret_cast<sockaddr*>(&si), sizeof(si));
 42     if (nRet == SOCKET_ERROR)
 43     {
 44         int nCode = WSAGetLastError();
 45         cout << "bind error! code = " << nCode << endl;
 46         closesocket(socketServer);
 47         WSACleanup();
 48         return 0;
 49     }
 50     
 51     nRet = listen(socketServer, SOMAXCONN);
 52     if (nRet == SOCKET_ERROR)
 53     {
 54         int nCode = WSAGetLastError();
 55         cout << "bind error! code = " << nCode << endl;
 56         closesocket(socketServer);
 57         WSACleanup();
 58         return 0;
 59     }
 60 
 61     while (1)
 62     {
 63         sockaddr_in siClient;
 64         int nLen = sizeof(siClient);
 65         SOCKET socketClient = accept(socketServer, reinterpret_cast<sockaddr*>(&siClient), &nLen);
 66         if (socketClient == INVALID_SOCKET)
 67         {
 68             cout << "accept Error, Code = " << WSAGetLastError() << endl;
 69             continue;
 70         }
 71         else
 72         {
 73             cout << "Has Connect SocketID = " << (int)socketClient << endl;
 74         }
 75         
 76         while (1)
 77         {
 78             char buf[1024] = { 0 };
 79             nRet = recv(socketClient, buf, 1024, 0);
 80             if (nRet == 0)
 81             {
 82                 cout << "Client Disconnect!" << endl;
 83                 closesocket(socketClient);
 84                 break;
 85             }
 86             else if (nRet == SOCKET_ERROR)
 87             {
 88                 cout << "Receive Client Data Error, Code = " << WSAGetLastError() << endl;
 89                 closesocket(socketClient);
 90                 break;
 91             }
 92             else
 93             {
 94                 cout << "Client : " << buf << endl;
 95 
 96                 std::string str;
 97                 cin >> str;
 98                 int nSned = send(socketClient, str.c_str(), str.length(), 0);
 99                 if (nSned == 0)
100                 {
101                     cout << "Client Disconnect!" << endl;
102                     closesocket(socketClient);
103                     break;
104                 }
105                 else if (nSned > 0 && nSned < str.length())
106                 {
107                     cout << "send partial data, length = " << nSned << endl;
108                 }
109                 else if (nSned == SOCKET_ERROR)
110                 {
111                     cout << "send error, code = " << WSAGetLastError() << endl;
112                     closesocket(socketClient); 
113                     break;
114                 }
115             }
116         }
117 
118     }
119 
120     closesocket(socketServer);
121     WSACleanup();
122     return 0;
123 }

客户端

客户端基本流程如下:

  1. 启动网络库
  2. 创建SOCKET
  3. 连接服务器
  4. 与服务器收发数据
  5. 退出,清理工作和关闭网络库

与服务端建立连接-connect函数

connect函数用于建立与远程主机的连接,该函数定义如下:

int connect(SOCKET s, const struct sockaddr* name, int namelen);

参数

  • s:要连接的套接字,调用connect后,该socket代表与远程主机之间建立的会话。
  • name:远程主机的地址信息,通过该字段指定要连接的主机。
  • namelen:name结构体的长度。

返回值

  • 若连接成功,则返回0。
  • 若连接失败,则返回SOCKET_ERROR,可以通过WSAGetLasterror()函数获取错误码。

简单实例

 1 #define _CRT_SECURE_NO_WARNINGS
 2 #define _WINSOCK_DEPRECATED_NO_WARNINGS
 3 
 4 #include <iostream>
 5 #include <WinSock2.h>
 6 #include <string>
 7 
 8 #pragma comment(lib, "ws2_32.lib")
 9 
10 using namespace std;
11 
12 const unsigned int marjorVersion = 2;
13 const unsigned int minorVersion = 2;
14 
15 SOCKET ServerSocket;
16 
17 BOOL WINAPI func(DWORD CtrlType)
18 {
19     if(CtrlType == CTRL_CLOSE_EVENT)
20     {
21         if (ServerSocket != INVALID_SOCKET)
22         {
23             closesocket(ServerSocket);
24             ServerSocket = INVALID_SOCKET;
25         }
26         WSACleanup();
27     }
28     return TRUE;
29 }
30 
31 int main()
32 {
33     SetConsoleCtrlHandler(func, TRUE);        //强制退出 自动关闭网络
34 
35     WORD wVersion = MAKEWORD(marjorVersion, minorVersion);
36     WSAData SocketVersionInfo;
37     int nRet = WSAStartup(wVersion, &SocketVersionInfo);
38     if (nRet != 0)
39     {
40         cout << "启动套接字失败" << endl;
41         return 0;
42     }
43 
44     ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
45     if (ServerSocket == INVALID_SOCKET)
46     {
47         int nErrorCode = WSAGetLastError();
48         std::cout << "套接字创建失败,错误码 " << nErrorCode << std::endl;
49         WSACleanup();
50         return 0;
51     }
52 
53     sockaddr_in addressInfo;
54     addressInfo.sin_port = htons(12345);
55     addressInfo.sin_family = AF_INET;
56     addressInfo.sin_addr.s_addr = inet_addr("127.0.0.1");
57     if (SOCKET_ERROR == connect(ServerSocket, reinterpret_cast<sockaddr*>(&addressInfo), sizeof(addressInfo)))
58     {
59         int nErrorCode = WSAGetLastError();
60         std::cout << "连接服务器失败,错误码 " << nErrorCode << std::endl;
61         closesocket(ServerSocket);
62         ServerSocket = INVALID_SOCKET;
63         WSACleanup();
64         return 0;
65     }
66 
67     while (1)
68     {
69         string str;
70         cout << "输入要发送的数据:" << endl;
71         cin >> str;
72         int nSendSize = send(ServerSocket, str.c_str(), str.length(), 0);
73         if (nSendSize == SOCKET_ERROR)
74         {
75             int nErrorCode = WSAGetLastError();
76             cout << "send error, error code " << nErrorCode << endl;
77             closesocket(ServerSocket);
78             ServerSocket = INVALID_SOCKET;
79             WSACleanup();
80             return 0;
81         }
82 
83         char buf[1024] = { 0 };
84         int nAcceptSize = recv(ServerSocket, buf, 1000, 0);
85         if (nAcceptSize != 0)
86             cout << "客户端:" << buf << endl;
87     }
88 
89     closesocket(ServerSocket);
90     ServerSocket = INVALID_SOCKET;
91     WSACleanup();
92     return 0;
93 }

总结

对于简单的C/S阻塞模型,使用accept()、recv()、connect()和send()等函数实现WinSock网络通信时,有如下缺点:

  • 这些函数都是阻塞的,同一时刻,直能与某一个客户进行数据交互,其他连接全部等待,无法实现并发。
  • 线程开销。我们可以将这些函数放到线程中处理,从而实现并发,但随着连接的增加,创建和管理大量线程会给系统带来负担,可能导致系统资源被耗尽。