问题 本篇将梳理前面讲过的所有网络模型,并在此基础上探究这些模型所涉及的设计思路。相当于前面只讲了What和How部分,这里专门来补充Why部分。因为若是一开始就从原理讲起难免枯燥乏味且晦涩难通,而有了前面的铺垫再学起来就能增加可预知性,这时你的知识网络里已经构建了网络编程的基本框架,已经建立了它们之间的联系。 这篇文章就希望在此基础上,帮助你一步一步对新东西进行扩展重构,加深理解。这样不论学起来还是使用起来,都能游刃有余,左右逢源。 那么首先就从最开始的基本Socket说起。 这个阶段所开发的网络程序都是同步程序,一个客户端连接进来,服务器阻塞地使用 accept 接受,之后阻塞地发送,阻塞地接收。 当一个客户端的数据没有处理完时,其它客户端都得阻塞地等待连接,这样的服务器设计同时只能处理一位客户。若处理操作很短,那么其它客户端倒不会感到不满,因为速度很快它们根本察觉不到差异。而一但处理耗时较长,其它客户端就满心不悦,尤其是处于后面连接的客户端,只有等到前面的所有客户端处理完成才能被轮到。 这就好像在一家只有一个窗口的餐馆排除买餐,先来者先买,排在最后面的只有等到前面所有人买完了之后才能轮到。若是速食,那么很快就能买到,所以轮到的也快;而若是慢食,第一个人就要2分钟才能买到,那么第十个人就要20分钟。 所以这种方式效率极低,基本满足不了多少需求,而优点则是开发最简单。 问题总是倒逼着解决方案,所以一个个的思路堪堪而现。 多进程服务器 第一种做法是创建多个进程来对连接的客户端提供服务,这种方式称为 多进程服务器。比如在Linux上你可以使用 fork 函数创建一个调用的进程副本,它会复制正在运行的调用 fork 函数的进程。调用 fork 函数的主体称为 父进程,而通过父进程复制出来的进程称为子进程,它们都将执行 fork 函数调用后的语句。 所以这种方式就是每有一个客户端连接,都由父进程创建一个子进程来处理。这就好似每有一位顾客来买餐,都为其提供一个专门的餐馆,所以需要付出的代价极大,需要进行大量的运算和内存空间,而且各个进程间的数据交换也需要复杂的方法来完成。 多线程服务器 第二种做法是每有一位客户端连接,都开启一条线程来为其服务,这种方式称为「多线程服务器」。 比如在之前所写的HTTP服务器中,我们就对每一个并发连接的客户端开启了一条专门的线程。在服务下一条请求之前,这个线程同步地完成一个请求操作。当客户端请求访问指定文件时,在线程中同步地读取文件,再发送给客户端。 这就像每有一位顾客来买餐,都为单独地开一个窗口。而服务人员是有限的(即CPU核心数),若有4位服务人员,那么你开8个窗口,这4个人就得在这些窗口之间来回切换。虽说开了8个窗口,其实处理速度并没有得到提升。 这时,若是让窗口的数量和服务人员一致,那么效率往往更好。还有在结账时,只能由一位服务人员去处理(共享资源),若是一人处理一半又有另一个人来处理就造成收了二次收费。 所以总结这个模式的缺点就是: 增加了性能开销(CPU频繁地进行上下文切换) 增加了同步复杂度 不可移植(并非所有系统都支持多线程,且系统之间实现可能有很大不同,难以达到行为一致) Reactor模式 为了提高服务器的效率,就得想办法用最少的消耗来处理更多的请求。在前面两种方法中,要么消耗大量内存,要么增加CPU开销,由于CPU性能和内存空间都有瓶颈,所以就造成了极大的浪费。 若能将所有请求都放到一个管理中心去统一管理,处理完成后再通知相应的客户那么将可节省很多资源。 举个例子,由于顾客日益增多,所以餐馆便安装了一个点餐系统。此时,顾客无需等待,可以直接点餐,点餐系统负责记录每位顾客的点餐信息,并分派给厨师去做。当餐做好后,再由点餐系统对相应顾客发出通知“请xxx到柜台取餐”,顾客只需竖起耳朵听着就好。 这就是「I/O多路复用」(I/O Multiplexing)的思路,也叫「事件驱动模型」(Event-driven)。 但是因为只有一位厨师,所以对于同时点餐的顾客,就不能同时处理,此时就要将点餐时间划分为非重叠空隙,在不同的时间段对用户进行处理。这就叫做「时分多路复用」(Time Division Multiplexing,TDM)。 若是通知一次比较费时,那么点餐系统不必在只有一位客户点餐时就通知厨师,可以一次性发送几位顾客的需求,之后厨师再分析出这几位顾客的需求。这就叫做「频分多路复用」(Frequency Division Multiplexing,FDM)。 前面说过,当餐做好后,点餐系统需要发出通知,那么此时必须从已记录的客户信息中解析出对应的客户,都能通知给正确的用户。那么这部分操作就叫做「多路分解」(Demultiplexing),多路复用与多路分解的关系可参考这张图: 可以看到,通过引用复用技术,减少了进程数与线程数,只需一条线程或进程来处理请求。 通过这个思路,经过不断探索与经验积累,形成了一套开发服务器的模式,而这个模式就是著名的「Reactor模式」(中译为反应器模式)。 Reactor模式允许请求事件被应用到多路复用并分派到服务请求中,所有请求被从一个或多个客户端投递到Reactor(即前面所述之管理中心),如此便反转了程序的控制流,程序无需主动去监听客户端的消息,只需等待Reactor通知便好。 那么现在来看看Reactor模式的UML结构图。… Continue Reading 各种网络模型背后的设计思路

前言 这是IOCP的末篇了,本次的实例使用IOCP配合扩展函数来实现服务器,并对之前的版本做一些优化,比如这里使用了内存池,日志记录,所以这也是效率最好的一个版本,作为一个例子来说已经很完整了。 因为前面已经介绍了较多的基础内容,并且也写出了实例一,所以这篇大部分内容都不会再详细讲解相关内容了,这写起来太费时间了。取而代之的是代码会全部列出来,若大家有哪里看不懂,那就说明前三篇未全部理解,这时应该再看看前面的文章。 类预览 首先来预览下类的定义,看看我们需要做些什么: #include <thread> // 线程库 #include <mutex> // 线程同步互斥量 #include <string> // 不用说 #include <memory> // 用到智能指针 #include <cassert> // 断言 #include <vector> // 使用vector保存连接的用户 #include <algorithm> // 算法库 #include <boost/pool/singleton_pool.hpp> // 内存池 #include <boost/format.hpp> // 格式化字符 #include <WinSock2.h> // winsock2 #include <MSWSock.h> // 扩展函数 #include… Continue Reading 网络模型之IOCP服务器实例二(四)

上篇用IOCP实现了一个简单的服务器,在处理消息方面性能已经不错了,但是接爱请求函数却依旧使用的是 accept 函数,所以这部分性能并不够,而Windows在扩展函数中为我们提供了一些选择,本篇就来介绍这些函数。 AcceptEx 这个函数用来异步地投递一个调用来接受客户端连接,其原型如下: BOOL AcceptEx ( SOCKET sListenSocket, // 监听套接字 SOCKET sAcceptSocket, // 分配给待连接客户端的套接字 PVOID lpOutputBuffer, // 用来接收用户第一份数据的缓冲区 DWORD dwReceiveDataLength, // 缓冲区的字节数 DWORD dwLocalAddressLength, // 本地套接字地址结构大小 DWORD dwRemoteAddressLength, // 远程套接字地址结构大小 LPDWORD lpdwBytesReceived, // 新建的客户机连接上所收到的字节数 LPOVERLAPPED lpOverlapped // 重叠结构 ); 这个函数稍微比 accept 函数麻烦了一点,那么在这里我们列出 accept 的原型来对比着展开介绍: SOCKET accept ( SOCKET… Continue Reading 网络模型之IOCP与扩展函数(三)

上篇简单地介绍了IOCP模型所需的基础内容,并给出了服务器版本一的声明,更多的内容会在本篇的实现中来展开说明,学完这篇就基本会明白怎样用IOCP来实现一个还不错的上万级别的服务器了。 开始之前得对上篇的类声明做一些改变,因为前天本来是用智能指针来管理客户端单句柄数据和单IO数据的,但当我那晚实现完后测试发现有点问题。当客户端程序低并发访问时没有任何问题,但在用脚本同时开很多客户端时有些客户端得不到正常关闭,这就说明服务器有问题了。开始我以为是线程同步的问题,跟踪了好久发现原来是智能指针的问题。在收到用户连接请求准备接收数据后就会准备接收下一位用户连接了,而在处理IO消息的线程中并非一直拥有指针的所有权,会在退出当前线程时丢失那么一小会儿,一当智能指针的引用计数变为0时原来分配的单IO数据就被释放了,但稍后却还需要用到它,因为已被释放所以稍后会导致访问已被释放的内存,就算能访问到里面也是一堆垃圾数据,后面的操作就得不到执行了,客户端也就无法正常关闭了。 所以在这种多线程中智能指针的使用也得格外小心,于是我换成了普通指针,之后没有任何问题。来看看改变后的类声明: class IOCPServer { public: IOCPServer(int port); ~IOCPServer(); void Accept(); private: void InitSock(); // 初始化套接字 void CreateIocp(); // 创建IOCP并开启线程 void AcceptHandler(); // 接受处理 void RequestHandler(); // IO消息处理线程 void RecvMsg(SOCKET, LPPER_IO_DATA); // 接收消息 void SendMsg(SOCKET, LPPER_IO_DATA, const std::string&); // 发送消息 void CloseSock(SOCKET, LPPER_HANDLE_DATA, LPPER_IO_DATA); // 关闭套接字 private: std::shared_ptr<PER_HANDLE_DATA> m_servSock;… Continue Reading 网络模型之IOCP实现版本一(二)

前言 前面介绍了重叠IO模型,该模型的缺点是发出IO请求的线程必须同时对完成通知进行处理,若一个线程发出多个请求,那么即使其它线程完全处于空闲状态,该线程也必须对每个请求的完成通知做出响应,从而影响了性能。 为了解决这个问题,我们可以在主线程中调用 accept 函数,再单独创建 1 个线程来负责客户端IO。实际上,IOCP模型便是创建了专用的线程来处理客户端IO,不过为了充分发挥 CPU 性能,它创建了不止一个线程。 所以IOCP模型其实就是对于重叠IO模型的更加完善,在阅读本篇之前,需要懂得如何使用重叠IO模型。 在学习各种网络模型之时,我们主要应该去关注两点,在select模型的时候大家就看过这个图: 实际上这两点也就是理解各个模型的关键,我们要关注如何等待客户端连接,还有如何将数据从网卡缓冲区拷贝到程序缓冲区。这两部分若有一部分阻塞,那么性能就不是太好。 IOCP(I/O Completion Port),即IO完成端口模型,该模型会创建一个CP(完成端口)内核对象,然后将CP对象与套接字绑定起来,之后套接字若有IO消息便可通过相关函数获得到。 创建CP对象 使用如下函数创建一个CP对象: HANDLE CreateIoCompletionPort ( HANDLE FileHandle, // file handle to associate with // the I/O completion port HANDLE ExistingCompletionPort, // handle to the I/O completion port DWORD CompletionKey, // per-file completion key for I/O… Continue Reading 网络模型之IOCP基础(一)

在上篇介绍了重叠 IO 的基本知识并使用事件的方式实现了第一个版本,但大家知道使用事件的缺点,因为 WaitForSingleObject 函数最多只能等待 64 个事件,所以要想处理更多的客户端得通过多个工作者线程来同时监视 Event 对象,这样便使处理更加麻烦,而且和事件选择其实一样依旧存在着部分阻塞。 所以今天来看第二种实现重叠 IO 的方法,即使用 Completion Routine。接着上篇,我们说这种方法是跟 WSASend 和 WSARecv 函数的最后一个参数有关的,所以再来看看其原型: int WSARecv ( SOCKET s, // 套接字句柄 LPWSABUF lpBuffers, // 指向待接收数据缓冲区 DWORD dwBufferCount, // lpBuffers数组的长度 LPDWORD lpNumberOfBytesRecvd, // 保存实际接收的字节数 LPDWORD lpFlags, // 数据传输标志 LPWSAOVERLAPPED lpOverlapped, // 指向重叠结构 LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE // 指向Completion Routine函数… Continue Reading 网络模型之重叠IO(二)

前面已经说过 select 和 Event selct 模型,这两个还是比较小型的,今天来说重叠 IO,这个可以支持上千个用户,当然理解起来也越来越难了点。 重叠 IO 模型是典型的非阻塞模型,接收数据和拷贝数据这两部分全部占用系统时间片,实现了效率最大化,只要将一个结构体投给系统它便会替我们完成耗时的处理。 首先来看如何创建一个支持重叠 IO 的套接字,使用的是 WSASocket 函数,原型如下: SOCKET WSASocket ( int af, // 地址族 int type, // 传输方式 int protocol, // 通信协议 LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags ); 前3个参数就是以前使用的 socket 函数的参数,主要来看后面的几个参数。lpProtocolInfo 可以传递一个 WSAPROTOCOL_INFO 结构体的地址,其中包含创建套接字的信息,不使用时传入 NULL;g 是作为扩展函数而预约的参数,也为 NULL 便好;dwFlags 表示套接字的标志,要使用重叠 IO,得设为 WSA_FLAG_OVERLAPPED。… Continue Reading 网络模型之重叠IO(一)

开始之前,再来看看流程图。 程序阻塞的主要有两部分,一个是等待数据到来,一个是将数据从内核复制到程序缓冲区。 事件选择模型,其实是在 select 模型的基础上更进一步地做了优化,这次优化把等待数据到来的这部分变成了非阻塞。而这主要是使用 WSAEventSelect 函数完成的,其原型如下: int WSAEventSelect ( SOCKET s, // 套接字 WSAEVENT hEventObject, // 事件对象 long lNetworkEvents // 网络事件 ); 事件选择给每个套接字绑定了一个事件,当发生指定网络事件的时候它会把绑定的事件变为 signaled 状态。 WSAEVENT 其实和之前在 Windows 线程同步中所说的内核对象事件是一样的,只是在这个函数中需要手动重置事件,为了清晰方便,所以弄了个 typedef,并提供了一个 WSACreateEvent 函数来创建手动重置的事件。 WSAEVENT event = WSACreateEvent(); 其实,这个 WSACreateEvent 函数就相当于调用以下语句: CreateEvent(NULL, TRUE, FALSE, NULL); lNetworkEvents 可以指定一些指定的网络事件,我们感兴趣的值如下: FD_READ FD_WRITE FD_ACCEPT FD_CONNECT… Continue Reading 网络模型之Event select

之前我们写了一些简单的网络程序,使用的都是基本的socket,这些程序有一个特点就是都是阻塞执行的。何谓阻塞呢?就是函数不会立即返回,直到等到结果才返回,像 accept,recv 就属于阻塞函数。 这种方式在人数(客户端)较少的情境下没有什么问题,但要是人数比较多了服务器就不能及时处理后面的客户请求了。若第一个连接的用户受理时间为 1 秒,那么第一百个连接的用户就得等 100秒。一个方法是可以为每个连接进来的用户开启一个线程进行处理,但开启太多的线程得付出很大的代价,所以这种方式也仅适用于用户较少的情况。 基于此,Windows 提供了一些网络模型来提高服务器同时接受客户端的能力。这里将会以几篇来分别讲解几个常用的模型: select 事件选择 重叠 IO IOCP 完成端口 这些模型从弱到强,从易到难。select 用来解决基本 socket 多线程的问题,适合 60 多个用户。事件选择异步了接受操作,适合 300-500 个用户。重叠 IO 就完全非阻塞了,适合上千个用户。IOCP 更加强大,可以支持万级别的用户,QQ 那些就使用的这个。 还有 Linux 上的 epoll 模型,这个和 IOCP 是一个级别的,因为我没怎么接触 Linux,所以这里就主要说 Windows 上的。 这里首先来说最简单的 select 模型,在此之前,我先画了一张图: 从图中可以看出有两个部分是阻塞的,一个是等待数据到来,一个是将数据从内核复制到程序缓冲区。 基本的 socket 这两个部分都是阻塞的,若是使用多线程的方式来处理,我们得用 n+1 个线程来服务 n 个客户。而申请大量线程代价极大且效率非佳,select 的目的就是来解决基本 socket… Continue Reading 网络模型之select

花开花落,倏忽一载便逝矣,新的一年,祝大家天天开心 HTTP即超文本传输协议(Hypertext Transfer Protocol),是Web通信所使用的协议。它是基于TCP/IP实现的协议,所以本篇需要先了解TCP通信,我们将使用TCP来写一个简单的Web服务器端,它可以响应浏览器的访问。 通信需要服务端和客户端,在这里浏览器就属于客户端,当访问一个网页时,浏览器内部会创建套接字和服务器进行通信。服务器会响应请求返回一些HTML格式的数据给浏览器,浏览器来把这些HTML数据解析成我们看到的漂亮的页面。 当我们在浏览器的地址栏上敲下一个域名地址后,浏览器会先通过默认DNS服务器获取该域名对应的IP地址,然后向服务器发送请求,请求有一定的标准,分为: 请求行 消息头 空行 消息体 现在来随便访问一个网址,这里使用的是 Firefox 浏览器,按 Ctrl+Shift+E 可以查看网络请求。 左边对应的是浏览器对服务器发出的请求,右边对应的是该条请求相关的信息。 我们先看左边的第一条,这里的请求方式是GET,表示想从服务器获取文件,获取的文件目录是/。然后服务器响应请求,发回了状态码302,表示Found重定向,我们本来访问的是 www.bing.com,现在被重定向到了 https://cn.bing.com/。这个重定向地址是通过响应头的location指定的,可以在右边看到。 https是加了SSL/TLS的协议,在需要安全的环境下,比如发送银行卡,身份信息等地方都会使用。只使用http这些信息很容易被窃听,SSL/TLS会对请求和响应的信息进行加密解密操作,保证数据安全。 接着同样是一些GET请求用于从服务器获取数据,状态码200 OK表示请求已经成功。还有常见的404 Not Found,表示找不到客户端请求的资源,这种情况我们就把链接转移到404错误页。现在来把对应的请求整理如下: 请求行: GET / HTTP/2.0 消息头: Accept:text/html,application/xhtml+xm…plication/xml;q=0.9,*/*;q=0.8 Accept-Encoding:gzip, deflate, br Accept-Language:zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Connection:keep-alive Cookie:_EDGE_V=1; MUID=3802FC7ABA5D6F…ndefined; _UR=OMD=13190641321 Host:cn.bing.com Upgrade-Insecure-Requests:1 User-Agent:Mozilla/5.0 (Windows NT 10.0; …) Gecko/20100101 Firefox/64.0 空行: 消息体: 请求行为一行数据,其中包含着请求方式,请求文件,HTTP版本。… Continue Reading HTTP协议之编写简单的Web服务器