郑州天道做网站,怎么做英文网站,wordpress视频适应播放器,游戏页面设计模板虽然市面上已经有很多成熟的网络库#xff0c;但是编写一个自己的网络库依然让我获益匪浅#xff0c;这篇文章主要包含#xff1a;TCP 网络库都干了些什么#xff1f;编写时需要注意哪些问题#xff1f;CppNet 是如何解决的。首先#xff0c;大家都知道操作系统原生的soc… 虽然市面上已经有很多成熟的网络库但是编写一个自己的网络库依然让我获益匪浅这篇文章主要包含TCP 网络库都干了些什么编写时需要注意哪些问题CppNet 是如何解决的。首先大家都知道操作系统原生的socket都是同步阻塞的你每调用一次发送接口线程就会阻塞在那里直到将数据复制到了发送窗体。那发送窗体满了怎么办阻塞的 socket 会一直等到有位置了或者超时。你每调用一次接收接口线程就会阻塞在那里直到接收窗体收到了数据。同步阻塞的弊端显而易见上厕所的时候不能玩手机不是每个人都能受得了。客户端可以单独建立一个线程一直阻塞等待接收那服务器每个 socket 都建一个线程阻塞等待岂不悲哉apache 这么用过所以有了 Nginx。那能不能创建一个异步的 socket 调用之后直接返回什么时候执行完了无论成功还是失败再通知回来实现所谓 IO 复用好消息是现在操作系统大都实现了异步 socketCppNet 中 Windows 上通过 WSASocket 创建异步的 socket在 Linux 上通过 fcntl 修改 socket 属性添加上 O_NONBLOCK。有了异步 socket调用的时候不论成功与否网络 IO 接口都会立马返回成功或失败发送了多少数据回头再通知你。现在调用是很舒畅那怎么获取结果通知呢这在不同操作系统就有了不同的实现。早些年的时候有过 select 和 poll但是各有各的弊端这个不是本文重点在此不再详述。现在在windows上使用 IOCP在 Linux 上使用 epoll 做事件触发基本已经算是共识。有了 IOCP 和 epoll我们调用网络接口的时候要把这个过程或者干脆叫做任务通知给事件触发模型让操作系统来监控哪个 socket 数据发送完了哪个 socket 有新数据接收了然后再通知给我们。到这里基本实现异步的socket读写该有的东西已经全部备齐。还有一点不同的是IOCP 在接收发送数据的时候会自己默默的干活儿干完了再通知给你。你告诉 IOCP 我要发送这些数据IOCP 就会默默的把这些数据写进发送窗体然后告诉你说“ 头儿我干完了 ” 。你告诉 IOCP 我要读取这个 socket 的数据IOCP 就会默默的接收这个socket的数据然后告诉你“头儿我给您带过来了”。这就着实让人省心你甚至不用再去调用 socket 的原生接口 。epoll 则不同其内部只是在监测这个socket是否可以发送或读取数据(当然还有建连等)不会像 IOCP 那样把活儿干完了再告诉你。你告诉 epoll 我要监测这个 socket 的发送和读取事件当事件到来的时候epoll 不会管怎么干活儿只会冷淡的敲敲窗户告诉你”有事儿了出来干活儿吧“。IOCP 像是一个懂得讨领导欢心的老油条epoll 则完全是一个初入职场的毛头小子。这就是 Proactor 和 Reactor 模式的区别。现在客户端就是领导的位置所以CppNet 实现为一个 Proactor 模式的网络库让客户端干最少的活儿。ASIO 也实现为 Proactor 而 libevent 实现为 Reactor 模式 。我们现在把刚才说的过程总结一下首先需要把 socket 设置非阻塞然后不同平台上将事件通知到不同事件触发模型上监测到事件时回调通知给上层。这就是一个网络库要有的核心功能所有其他的东西都是在给这个过程做辅助。听起来非常简单接下来就说下编写网络库的时候会遇到哪些问题和CppNet的实现。首先的问题是跨平台如何抽象操作系统的接口对上层实现透明调用。不论是 epoll 还是 socket 接口Windows 和 Linux 提供的接口都有差异如何做到对调用方完全透明这就需要调用方完全知道自己需要什么功能的接口然后将自己需要的接口声明在一个公有的头文件里在定义时 CppNet 通过 __linux__ 宏在编译期选择不同的实现代码。__linux__ 宏在 Linux 平台编译的时候会自动定义。如果不是上层必须的接口则不同平台自己定义文件实现内部消化不会让上层感知。网络事件驱动抽象出一个虚拟基类提前声明好所有网络通知相关接口不同平台自己继承去实现。Nginx 虽然是 C 语言编写但是通过函数指针来实现类似的构成。大家已经知道 epoll 和 IOCP 是不同模式的事件模型如何把 epoll 也封装成 Proactor 模式这就需要要在 epoll 之上添加一个实际调用网络收发接口的干活儿层。CppNet 实现上分为三层不同层之间通过回调函数向上通知。其中网络事件层将 epoll 和 IOCP 抽象出相同的接口在 socket 层不同平台上做了不同的调用Windows 层直接调用接口将已经接收到的数据拷贝出来而 Linux 平台则需要在收到通知时调用发送数据接口或者将该 socket 接收窗体的数据全部读取而出。为什么要将数据全部读取出来这又设计到 epoll 的两种触发模式水平触发和边缘触发。水平触发( LT ) 只要有一个 socket 的接收窗体有数据那么下一轮 epoll_wait 返回就会通知这个 socket 有读事件触发。意味着如果本次触发读取事件的时候没有将接收窗体中的数据全部取出那么下一次 epoll_wait 的时候还会再通知这个 socket 的读取事件即使两次调用中间没有新的数据到达。边缘触发( ET ) 一个 socket 收到数据之后只会触发一次读取事件通知若是没有将接收窗体的数据全部读取那么下一轮 epoll_wait 也不会再触发该 socket 的读事件而是要等到下一次再接收到新的数据时才会再次触发。水平触发比边缘触发效率要低一些在 epoll 内部实现上用了两个数据结构用红黑树来管理监测的 socket每个节点上对应存放着 socket handle 和触发的回调函数指针。一个活动 socket 事件链表当事件到来时回调函数会将收到的事件信息插入到活动链表中。边缘触发模式时每次 epoll_wait 时只需要将活动事件链表取出即可但是水平触发模式时还需要将数据未全部读取的 socket 再次放置到链表中。CppNet 采用的是边缘触发模式。边缘触发在读取数据的时候有个问题叫做读饥渴何为读饥渴读饥渴就是如果两个 socket 在同一个线程中触发了读取事件而前一个 socket 的数据量较大后一个 socket 就会一直等待读取对客户端看来就是服务器反应慢。凡事无完美 究竟选择哪种模式具体如何取舍就需要更多业务场景上的考量了。前面提到IOCP 不光负责的干了数据读取发送的活儿甚至还兼职管理了线程池。在初始化 IOCP handle 的时候有一个参数就是告知其创建几个网络 IO 线程但是 epoll 没有管这么多。在编写网络库的时候就需要考虑是将一个 epoll handle 放在多个线程中使用还是每个线程都建立一个自己的 epoll handle如果每个线程一个 epoll handle 则所有接收到的客户端 socket 终其一生都只会生活在一个线程中连接数据交互直到销毁具体处于哪个线程则交给了内核控制(通过端口复用处理惊群)这就会导致线程间负载不均衡因为 socket 连接时长数据大小都可能不同但是锁碰撞会降到最低。如果所有线程共享一个 epoll handle则要考虑线程数据同步的问题如果一个 socket 在一个线程读取的时候又在另一个线程触发了读取该如何处理epoll 可以通过设置 EPOLLONESHOT 标识来防止此类问题设置这个标识后每次触发读取之后都需要重置这个标识才会再次触发。人生就是一个不断选择的过程没有最完美只有最合适。CppNet 可以通过初始化时的参数控制在 Linux 实现上述两种方式。一直再说数据读取的事儿下面说说建立连接。大家知道服务器上创建 socket 之后绑定地址和端口然后调用 accept 来等待连接请求。等待意味着阻塞前边已经提到了我们用到的 socket 已经全部设置为非阻塞模式了你调用了 accept也不会乖乖的阻塞在哪里了而是迅速返回有没有连接到来还得接着判断。这么麻烦的事情当然还是交给操作系统来操作和数据收发相同我们也把监听 socket 放到事件触发模型里但是要放到哪个里呢IOCP 只有一个 handle所以没的选择我们投递了监听任务之后IOCP 会自己判断从哪个线程中返回建立连接的操作。epoll 则又是道多选题如果用了每个线程一个 epoll handle 的模式所有线程都监测着监听的 socket那么连接到来的时候所有线程都会被唤醒是为惊群。这个可以借鉴一下 Nginx通过一个简单的算法来控制哪些线程(Nginx 是进程)去竞争一个全局的锁竞争到锁的线程将监听 socket 放置到 epoll 中顺带着还均衡了一下线程的负载。现在我们有了另外一个选择通过设置 socket SO_REUSEADDR 标识让多个 socket 绑定到同一个端口上让操作系统来控制唤醒哪个线程。写到现在连接数据收发已经基本实现该如何管理收发数据的缓存呢随时抛给上层还是做个中间缓存这又涉及到一个拆包的问题大家知道TCP 发送的是 byte 流并没有包的概念如果你把半个客户端发送来的的消息体返回给服务器服务器也没有办法执行响应操作只能等待剩下的部分到来。所以最好是加一层缓存这个缓存大小无法提前预知需要动态分配还要兼顾效率减少复制。CppNet 在 socket 层添加了 loop-buffer 数据结构来管理接收和发送的字节流。实现如其名底层是来自内存池的固定大小内存块通过两个指针控制来循环的读写上层是一个由刚才所说的内存块组成的链表也通过两个指针控制来循环读写。这样每次添加数据时都是顺序的追加操作没有之前旧数据的移动实现最少的内存拷贝。那有了缓存之后如何快速的将要发送和接收的数据放置到缓存区呢我一开始是直接在 recv 和 send 的地方建立一个栈上的临时缓存读取到数据之后再将栈缓存上的数据写到 loop-buffer 上这样无疑多了一次数据复制的代价。Linux系统提供了 writev 和 readv 接口集中写和分散读每次读写的时候都直接将申请好的内存块交给内核来复制数据然后再通过返回值移动指针来标识数据位置配合 loop-buffer 相得益彰。CppNet 前后历时半载历经两司到现在终于有所小成作文以记之。githubhttps://github.com/caozhiyi/CppNet来源https://zhuanlan.zhihu.com/p/80634656