C/S阻塞模型是指客户端/服务器阻塞模型,它描述了一种基于阻塞的网络通信方式。在阻塞模型中,客户端发送请求给服务器,并等待服务器的响应。在等待服务器响应的过程中,客户端的操作会被阻塞,直到服务器响应返回或超时。
服务器
服务器基本流程如下:
- 启动网络库
- 创建服务器Socket
- 绑定服务器地址和端口号
- 进入监听模式
- 接收客户端连接请求
- 与客户端进行通信
- 退出,清理工作和关闭网络库
创建套接字-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 }
客户端
客户端基本流程如下:
- 启动网络库
- 创建SOCKET
- 连接服务器
- 与服务器收发数据
- 退出,清理工作和关闭网络库
与服务端建立连接-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网络通信时,有如下缺点:
- 在这些函数都是阻塞的,同一时刻,直能与某一个客户进行数据交互,其他连接全部等待,无法实现并发。
- 线程开销。我们可以将这些函数放到线程中处理,从而实现并发,但随着连接的增加,创建和管理大量线程会给系统带来负担,可能导致系统资源被耗尽。
标签:02,addr,函数,int,模型,阻塞,接字,客户端,SOCKET From: https://www.cnblogs.com/BroccoliFighter/p/17870932.html