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网络通信时,有如下缺点:

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

二:简单的C/S阻塞模型的更多相关文章

  1. Python的异步编程[0] -> 协程[1] -> 使用协程建立自己的异步非阻塞模型

    使用协程建立自己的异步非阻塞模型 接下来例子中,将使用纯粹的Python编码搭建一个异步模型,相当于自己构建的一个asyncio模块,这也许能对asyncio模块底层实现的理解有更大的帮助.主要参考为 ...

  2. Java线程(学习整理)--4---一个简单的生产者、消费者模型

     1.简单的小例子: 下面这个例子主要观察的是: 一个对象的wait()和notify()使用情况! 当一个对象调用了wait(),那么当前掌握该对象锁标记的线程,就会让出CPU的使用权,转而进入该对 ...

  3. 程序员必知的8大排序(二)-------简单选择排序,堆排序(java实现)

    程序员必知的8大排序(一)-------直接插入排序,希尔排序(java实现) 程序员必知的8大排序(二)-------简单选择排序,堆排序(java实现) 程序员必知的8大排序(三)-------冒 ...

  4. IO阻塞模型 非阻塞模型

       IO阻塞模型(blocking IO) 在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:  所以,blocking IO的特点就是在IO执行的两 ...

  5. IO阻塞模型、IO非阻塞模型、多路复用IO模型

    IO操作主要包括两类: 本地IO 网络IO 本地IO:本地IO是指本地的文件读取等操作,本地IO的优化主要是在操作系统中进行,我们对于本地IO的优化作用十分有限 网络IO:网络IO指的是在进行网络操作 ...

  6. fcntl()函数之非阻塞模型

    优点:设置标准输入为非阻塞(有数据则读 没有数据则立即返回),常用于网络通信以及轻量信息多并发中 步骤: 1.oldflag=fcntl(STDIN_FILENO,F_GETFL); 获取标准输入的文 ...

  7. Html学习笔记(二) 简单标签

    标签的重点 标签的用途 标签在浏览器中的默认样式 <body>标签: 在网页上显示的内容 <p>标签: 添加段落 <hx>标签: 添加标题 标签一共有6个,h1.h ...

  8. Linux非阻塞IO(二)网络编程中非阻塞IO与IO复用模型结合

    上文描述了最简易的非阻塞IO,采用的是轮询的方式,这节我们使用IO复用模型.   阻塞IO   过去我们使用IO复用与阻塞IO结合的时候,IO复用模型起到的作用是并发监听多个fd. 以简单的回射服务器 ...

  9. 服务器端IO模型的简单介绍及实现 阻塞 / 非阻塞 VS 同步 / 异步 内核实现的拷贝效率

    小结: 1.在多线程的基础上,可以考虑使用"线程池"或"连接池","线程池"旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲 ...

  10. java web学习总结(二十二) -------------------简单模拟SpringMVC

    在Spring MVC中,将一个普通的java类标注上Controller注解之后,再将类中的方法使用RequestMapping注解标注,那么这个普通的java类就够处理Web请求,示例代码如下: ...

随机推荐

  1. Deepseek学习随笔(4)--- DeepSeek 在学术中的应用

    文献阅读与总结 对于学术研究人员来说,DeepSeek 可以帮助快速阅读和总结文献: 上传 PDF 文献,输入:请总结本文的研究方法和核心结论 DeepSeek 会生成文献的摘要,帮助你快速了解主要内 ...

  2. 深入理解 Docker 容器技术

    一.引言 在当今的云计算和软件开发领域,Docker 容器技术已经成为了一项不可或缺的工具.它极大地改变了应用程序的部署和运行方式,为开发者和运维人员带来了诸多便利. 二.Docker 容器是什么? ...

  3. vue - [02] 安装部署

    Vue.js 是一个流行的前端JavaScript框架,用于构建用户界面. 001 || 通过CND快速开始 只需要在HTML文件中引入VUE的CDN链接即可 (1)创建HTML文件 <!DOC ...

  4. camunda工作流实战项目(表单设计器+流程编辑器,零代码创建流程)

    该项目的plus版本已制作完成,文章链接 [plus版]camunda工作流实战项目 一.整体情况介绍 基于ruoyi平台和camunda工作流开发而成,结合bpmn.js流程编辑器和vform表单设 ...

  5. Linux 通过docker安装nginx,.net core sdk或运行时安装到Linux

    1.Linux docker通过yum安装 https://blog.csdn.net/GMingZhou/article/details/94024453 https://qizhanming.co ...

  6. [Qt基础-07 QSignalMapper]

    QSignalMapper 本文主要根据QT官方帮助文档以及日常使用,简单的介绍一下QSignalMapper的功能以及使用 文章目录 QSignalMapper 简介 使用方法 主要的函数 信号和槽 ...

  7. mysql grant 用户权限

    用户添加授权 mysql> grant all privileges on *.* to 'niuben'@'%' identified by '123456' with grant optio ...

  8. http://eslint.org/docs/rules/semi

    报错: Errors: 88 http://eslint.org/docs/rules/semi 56 http://eslint.org/docs/rules/quotes 34 http://es ...

  9. HTTP 和 RPC

    TCP 是传输层的协议,而基于 TCP 造出来的 HTTP 和各类 RPC 协议,它们都只是定义了不同消息格式的应用层协议而已. RPC(Remote Procedure Call),又叫做远程过程调 ...

  10. Effective Java理解笔记系列-第2条-何时考虑用构建器?

    为什么写这系列博客? 在阅读<Effective Java>这本书时,我发现有许多地方需要仔细认真地慢慢阅读并且在必要时查阅相关资料才能彻底搞懂,相信有些读者在阅读此书时也有类似感受:同时 ...