ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TCPsocket编程从accept到线程池连接型服务器的第一课

TCPsocket编程从accept到线程池连接型服务器的第一课 TCP socket 编程从 accept 到线程池连接型服务器的第一课我的github(https://github.com/xcx55/ubuntu-linux-project)感谢各位大佬参观我的githubUDP 那条线走完后8 月 30 号到 9 月 1 号进入了 TCP 编程。和 UDP 相比TCP 的接口多了一层连接的概念而这一层连接带来了一整套新的问题服务端怎么同时服务多个客户端串行、多进程、多线程、线程池该怎么选这篇按笔记的推进顺序整理。一、TCP 和 UDP 编程模型的本质差异先抓住一句话核心UDP 是一个 socket 文件打天下TCP 是一个监听文件 一堆连接文件。UDP数据直接写进 socket 文件里报文自带sockaddr_in——每个报文都自带地址recvfrom一次拿数据一次拿对端地址谁发的就回给谁TCP先listen connect建立连接然后accept返回一个新的 fd后续的读写都发生在这个新 fd 上。具体到 accept 的动作accept等待客户端的connect拿到客户端的sockaddr_in把这个sockaddr_in交给当前打开的 sockfd 文件记录再打开一个新 file作为后续 read/write 的对象。一个有意思的对照TCP 和 UDP 拿取 sockaddr_in 的方式一模一样——区别只在于 UDP 是每个报文都带TCP 是连接建立时带一次、终身有效。另一个对照listensockfd一个文件服务多个 ip 和端口——它是接客的门童自己不运货运货的是 accept 返回的那一个个连接 fd。还有一条容易被忽略的底层事实read 函数会分别判断 fd 的类型——管道、普通文件、网络文件TCP 的文件各走各的分支。这就是一切皆文件的代价与回报接口统一内部分流。二、服务多个客户端四种姿势的演进客户端一多问题立刻来了如果在一个循环里串行地ServerIO(fd, pq)一个客户端没处理完下一个 accept 就被堵着——串行执行是阻塞的。笔记里把解决方案按历史顺序排了一遍1. 串行最朴素while(true) { accept → ServerIO(fd, pq) → close(fd); }。能用但一次只能伺候一个人。2. 多进程每个客户端 fork 一个子进程去处理。要配一句signal(SIGCHLD, SIG_IGN)让内核自动回收子进程否则僵尸进程堆积。但笔记里直言这样很容易被搞挂掉——fork 的成本摆在那。3. 多线程比 fork 便宜但长服务下会线程溢出。这里笔记里给出了一组关键概念短服务处理逻辑短线程滞留时间短线程不会太溢出“比较清爽”——平时的登录注册都是短服务长服务一个微信要是 1000000 个客户端连过来一个连接一个线程服务器不得炸了4. 线程池把线程数量钉死任务进队列voidStart(){signal(SIGCHLD,SIG_IGN);threadpoolcallback_t*pathreadpoolcallback_t::Getthreadpool();while(true){sockaddr_in pq;socklen_t asizeof(sockaddr_in);intfdaccept(_listensockfd,(sockaddr*)pq,a);// listen内部if(fd0){std::coutaccept errstd::endl;continue;}pa-Enqueue([fd,pq,this]()-void{this-ServerIO(fd,pq);});}}一句话点评接入线程池限制了效率上限但服务器不会挂掉——用峰值性能换稳定性这笔账在服务端永远是划算的。线程池版本还带来一个设计红利回调可以在 ServerIO 里再次包装——比如处理指令就是远程处理类业务逻辑通过 lambda 注册进来服务器骨架完全不用改。这正好接上了线程池那篇回调 底层调用上层的结论。三、connect 的隐式绑定UDP 那篇讲过 sendto 会隐式 bindTCP 这边对称地来了一个connect 和 sendto 一样会把 sockfd 进行隐式绑定——客户端不用显式 bindconnect 时若还没绑定相当于pa nullptr就进行默认绑定并默认发送。规则是通用的主动发起通信的一方端口交给 OS 随机分配。四、地址转换函数的两个坑inet_ntoa 为什么不可重入inet_ntoa把 4 字节整数 IP 转成点分十进制字符串用起来很顺手但它有个隐蔽的问题返回的 char是函数内部 static 的所以它是不可重入函数。*推演一下事故现场线程 A 调用inet_ntoa还没来得及使用返回值线程 B 也调用了inet_ntoa——static 缓冲区被覆盖线程 A 拿到的是别人的 IP 字符串。而inet_ntoa没有加锁这个坑是无声的。正解是用这一对inet_ntop()网络序整数 → 字符串n networkp presentation进程序列多传一个缓冲区参数inet_pton()字符串 → 网络序整数。缓冲区 库内的锁同时解决了线程安全和存储安全——调用者自己的缓冲区谁也覆盖不了。这个用调用者提供的缓冲区代替函数内 static 缓冲区的手法是可重入函数设计的标准范式和信号处理那篇不可重入函数的讨论完全呼应。五、小结UDPTCPsocket 文件一个读写都在它listensockfd 每连接一个 fd对端地址每个报文自带accept 时拿到一次客户端 bindsendto 隐式绑定connect 隐式绑定多客户端无连接天然并发串行 → 多进程 → 多线程 → 线程池推荐的地址转换inet_ntop / inet_pton不要用 inet_ntoa不可重入TCP 编程的第一课其实不是接口而是**连接这个概念给服务端带来的并发压力**——UDP 天生无连接无所谓并发模型TCP 每一个连接都是一份要长期持有的资源于是才有短服务/长服务的取舍和线程池的登场。而连接维持住了之后新的问题马上出现TCP 发的数据是一片一片的读到的东西可能半截——这就引出了下一篇的主角序列化与自定义协议。
返回列表