前言

  多年前开发了一套网络库,底层实现采用IOCP(完成端口)。该库已在公司多个程序中应用;经过多次修改,长时间检验,已经非常稳定高效。

最近把以前的代码梳理了一下,又加进了一些新的思路。代码结构更加合理,性能也有所提升。打算将该库一些的知识点写出来,以供参考。

服务端要在多个端口监听,这种场合并不多见。但作为一个完善的网络库,似乎有必要支持此功能的。

传统实现方法

  如果监听端口个数很少,也可以采用传统的方法。因为accept函数是阻塞的,所以要实现在n个端口监听,就需要n个线程。如果监听端口个数不多,这也不是多大问题。如果监听端口多达几十个,这种方法就有些不妥。线程也是一种资源,线程过多占用资源会增加;也会导致系统负担加重。

更可行的实现方法

  实现方法有些曲折,需要一步一步分析;基本的原理就是将socket句柄与事件(event)相关联。Windows有相关的函数可以对多个事件监听,当某个事件被触发,就知道相应的socket有事件到达。可以对该socket做accept,因为已经确定该socket有事件了,所以accept函数会立即返回。这样就达到对多个端口同时监听的目的。

1)生成socket,并与某个端口绑定

  1. struct LISTEN_SOCKET_INFO
  2. {
  3. UINT16 listenPort; //监听端口
  4. SOCKET listenSocket;//句柄
  5. WSAEVENT netEvent; //socket对应事件
  6. };
  7.  
  8. int IocpAccept::CreateListenInfo()
  9. {
  10. //m_listListenPort存储要监听的端口;总个数不超过64个
  11. std::vector<UINT16>::iterator pos = m_listListenPort.begin();
  12. for (;pos != m_listListenPort.end();++pos)
  13. {
  14. //生成socket
  15. UINT16 listenPort = *pos;
  16. LISTEN_SOCKET_INFO socketInfo;
  17. socketInfo.listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  18. socketInfo.listenPort = listenPort;
  19.  
  20. //绑定端口
  21. sockaddr_in InetAddr;
  22. InetAddr.sin_family = AF_INET;
  23. InetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
  24. InetAddr.sin_port = htons(listenPort);
  25.  
  26. int ret = bind(socketInfo.listenSocket, (SOCKADDR *)&InetAddr, sizeof(InetAddr));
  27. if (SOCKET_ERROR == ret)
  28. {
  29. ::closesocket(socketInfo.listenSocket);
  30. //绑定失败
  31. continue;
  32. }
  33.  
  34. //生成事件
  35. socketInfo.netEvent = WSACreateEvent();
  36.  
  37. //将socket句柄与事件关联起来。只监视socket的accept和close消息
  38. ret = WSAEventSelect(socketInfo.listenSocket, socketInfo.netEvent, FD_ACCEPT | FD_CLOSE);
  39. if (SOCKET_ERROR == ret)
  40. {
  41. ::closesocket(socketInfo.listenSocket);
  42. continue;
  43. }
  44.  
  45. // 启动监听
  46. ret = listen(socketInfo.listenSocket, );
  47. if (SOCKET_ERROR == ret)
  48. {
  49. ::closesocket(socketInfo.listenSocket);
  50. continue;
  51. }
  52.  
  53. m_listListenInfo.push_back(socketInfo);
  54. }
  55. return ;
  56. }

该函数已将需要的数据存储在列表m_listListenInfo中。

2)启动监听线程,对多个事件监听

  对多个事件监听用到如下函数:

DWORD WSAAPI WSAWaitForMultipleEvents( DWORD cEvents, const WSAEVENT *lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable );该函数最多可以对64个事件做跟踪,所以一个线程最多可以对64个端口做监听。(同时对超过64个端口监听的场合非常少见。本文不考虑。)

  1. //生成事件地址指针
  2. int nEventTotal;
  3. WSAEVENT* pEventArray = CreateNetEventArray(&nEventTotal);
  4. if (nEventTotal == )
  5. return ;
  6. assert(nEventTotal <= WSA_MAXIMUM_WAIT_EVENTS);
  7.  
  8. MSG msg;
  9. while (m_bServerStart)
  10. {
  11. // 同时对多个事件做监听
  12. DWORD index = WSAWaitForMultipleEvents(nEventTotal,
  13. pEventArray,
  14. FALSE,
  15. ,
  16. FALSE);
  17. if (!m_bServerStart)
  18. return ;
  19.  
  20. //查看是哪个事件触发函数返回
  21. index = index - WSA_WAIT_EVENT_0;
  22. //客户端连接事件
  23. if ((index != WSA_WAIT_FAILED) && (index != WSA_WAIT_TIMEOUT))
  24. {
  25. //pEventArray排序与m_listListenInfo一样,所以可以根据index找到对应的socket。
  26. //就是该socket导致函数返回
  27. LISTEN_SOCKET_INFO socketInfo = m_listListenInfo[index];
  28.  
  29. //查看具体是什么事件导致函数返回
  30. WSANETWORKEVENTS NetworkEvents;
  31. WSAEnumNetworkEvents(socketInfo.listenSocket, pEventArray[index], &NetworkEvents);
  32.  
  33. //如果是accept事件,说明有客户端连接此端口
  34. if (NetworkEvents.lNetworkEvents == FD_ACCEPT
  35. && NetworkEvents.iErrorCode[FD_ACCEPT_BIT] == )
  36. {
  37. //这时调用accept函数,会立即返回
  38. AcceptListenPort(socketInfo.listenSocket, socketInfo.listenPort);
  39. }
  40. if (NetworkEvents.lNetworkEvents == FD_CLOSE
  41. && NetworkEvents.iErrorCode[FD_CLOSE_BIT] == )
  42. {
  43. assert(false);
  44. }
  45.  
  46. }
  47. else
  48. {
  49. //因为超时等其他原因引起函数返回
  50. }
  51. }

下文accept函数调用,并不会阻塞。

  1. UINT IocpAccept::AcceptListenPort(SOCKET hListenSocket, UINT16 nListenPort)
  2. {
  3. SOCKET hClient = ;
  4. SOCKADDR_IN localAddr;
  5. int iaddrSize = sizeof(SOCKADDR_IN);
  6.  
  7. hClient = accept(hListenSocket, (struct sockaddr *)&localAddr, &iaddrSize);
  8. if (INVALID_SOCKET == hClient)
  9. {
  10. int nAccepetError = WSAGetLastError();
  11. if (nAccepetError == WSAECONNRESET)
  12. {
  13. return ;
  14. }
  15. else
  16. {
  17. return ;
  18. }
  19. }
  20. else
  21. {
  22. //获取了一个客户端连接
  23. OnAcceptClient(hClient, nListenPort);
  24. }
  25. return ;
  26. }

后记:同时对多个端口做监听,可能还有更好的方法。如果对几百个以上端口做监听,此方法可能就不太合适。通常情况下,对多个端口监听的场景比较少见,所以对更优化的处理方法也没深究。

代码下载地址:https://download.csdn.net/download/qq_29939347/10691921

单线程实现同时监听多个端口(windows平台c++代码)的更多相关文章

  1. Memcached监听多个端口_同一台Windows机器中启动多个Memcached服务

    下载Memcached服务器 假设你解压在"C:\Program Files\memcached\memcached.exe" 那么可以如下创建多个服务监听不同的端口啦 监听第一个 ...

  2. wriesharek同时监听多个端口

    之前的文章<wireshark解析自定义的protobuf协议> ,当时只监听了一个端口,而如果游戏同时有二个 socket 连接,比如一个是网关另外一个是其它的,怎么办呢? for i, ...

  3. linux下启动多个php,分别监听不同的端口。

    在工作中,我们可能会遇到,服务器集群的搭建. 这个时候,我们不可能,每一台服务器都是lnmp的环境,我们会把nmp分别放在不同的服务器上,不同的服务器负责不同的功能.比如我们下面要说的php 加入ng ...

  4. prometheus 监控 jar应用服务 + 修改监听IP和端口

    1.修改服务的启动脚本 [root@do1cloud01 init.d]# vim learn-school nohup ${JAVA_HOME}/bin/java -javaagent:/usr/l ...

  5. apache反向代理和监听多个端口设置

    修改apache配置文件httpd.conf 一.监听多个端口 在Listen 80后添加监听端口,如 Listen 80 Listen 8080 Listen 8008 二.反向代理设置 1.取消一 ...

  6. 同一台机器oralce11g和12c公用一个监听器监听多个端口

    启动数据库服务 (这里还没有启动监听器) 如上图OracleServiceORCL为11g的服务,服务名为orcl,OracleServiceWX为12c的服务名,服务名为wx,两个服务均已正常启动, ...

  7. ssh 监听多个端口

    修改sshd的配置文件 默认位置:/etc/ssh/sshd_config 注释掉 Port 这行   然后添加 ListenAddress 行 e.g:  ListenAddress 192.168 ...

  8. CentOS7 配置 SSH监听多个端口方法

    一.修改ssh默认端口,防止暴力破解,让系统安全多一点点: i. 在配置文件/etc/ssh/sshd_config文件中修改 Port #AddressFamily any #ListenAddre ...

  9. 本地启动服务,两个进程分别监听两个端口,导致两个 URL 不同

    问题描述: 本地启了两个服务:A(http://localhost:8001) B(http://localhost:8000),A 项目要怎么才能关联到 B 项目,也就是 A 项目请求怎么跳到 B ...

随机推荐

  1. JAVA-部署-摘

    一.下载 J2SDK下载http://java.sun.com/j2se/1.4.2/download.html  下载版本是j2sdk-1_4_2_08 ECLIPSE下载http://www.ec ...

  2. day32(表单校验js和jquery表单校验)

    校验用户名.密码.密码一直性. <style> .error { color: red } .success { color: green } </style> <scr ...

  3. day18(javaEE三大组件之一servlet(简介(一)))

    Servlet servlet是小型服务器语言,使用它可以处理前台传递来的信息,servlet进行处理后在响应给前台,其中servlet起到了关键性的作用.前端输入的信息可以持久化的存储在数据库中,并 ...

  4. _编程语言_C++_setw()

    C++ 中使用setw(int n) 来控制输出间隔. 例如: cout<<)<<'a'<<endl;//s与a之间有7个空格,setw()只对后面紧跟的输出产生作 ...

  5. 第83讲:Scala中List的实现内幕源码揭秘

    今天我们来学习一下scala的List的方法的内部源码的一些知识. 首先,take方法.take方法就是取列表的从第一个元素开始的前N个元素.如list.take(3),就是取list的前3个元素,返 ...

  6. CSS 基础 例子 display属性:block、inline和inline-block的区别

    HTML中块级元素(block)和行级元素(inline):比如div就是常见的块级元素,span就是常见的行级元素. 可以通过css的display属性来设置一个元素到底是块级,还是行级元素:dis ...

  7. Winform文件上传

    近期在做了一个winform的项目的附件上传的需求 最初项目选型的时候有三种 1.使用webservice.webapi上传文件 2,直接保存在数据库中 3.使用共享目录+dos命令 第一种有文件大小 ...

  8. 《AngularJS深度剖析与最佳实践》笔记: 第二章 概念介绍

    第二章 概念介绍 2.1 什么是UI? 用户界面包括内容(静态信息+动态信息), 外观, 交互. 在前端技术栈中分别由HTML, CSS和JS负责. 进一步抽象, 分别对应于MVC三个主要部分: Mo ...

  9. Android RelativeLayout wrap_content 而且 child view 使用 layout_alignParentBottom 时 RelativeLayout 高度会占满屏幕

    Android RelativeLayout wrap_content 而且 child view 使用 layout_alignParentBottom 时 RelativeLayout 高度会占满 ...

  10. C/C++掌握技能(一)

    1.在编译器中输入代码并将其保存为.cpp文件(C语言的文件扩展名.c,但为了使用C++中的一些好用的特性,请把文件扩展名改为C++的.cpp)2.等价头文件:#include<stdio.h& ...