1、首先需要一个内存池,目的在于:
·减少频繁的分配和释放,提高性能的同时,还能避免内存碎片的问题;
·能够存储变长的数据,不要很傻瓜地只能预分配一个最大长度;
·基于SLAB算法实现内存池是一个好的思路:分配不同大小的多个块,请求时返回大于请求长度的最小块即可,对于容器而言,处理固定块的分配和回收,相当 容易实现。当然,还要记得需要设计成线程安全的,自旋锁比较好,使用读写自旋锁就更好了。
·分配内容的增长管理是一个问题,比如第一次需要1KB空间,随着数据源源不断的写入,第二次就需要4KB空间了。扩充空间容易实现,可是扩充的时候必然 涉及数据拷贝。甚至,扩充的需求很大,上百兆的数据,这样就不好办了。暂时没更好的想法,可以像STL一样,指数级增长的分配策略,拷贝数据虽不可避免, 但是起码重分配的几率越来越小了。
·上面提到的,如果是上百兆的数据扩展需要,采用内存映射文件来管理是一个好的办法:映射文件后,虽然占了很大的虚拟内存,但是物理内存仅在写入的时候才 会被分配,加上madvice()来加上顺序写的优化建议后,物理内存的消耗也会变小。
·用string或者vector去管理内存并不明智,虽然很简单,但服务器软件开发中不适合使用STL,特别是对稳定性和性能要求很高的情况下。
2、第二个需要考虑的是对象池,与内存池类似:
·减少对象的分配和释放。其实C++对象也就是struct,把构造和析构脱离出来手动初始化和清理,保持对同一个缓冲区的循环利用,也就不难了。
·可以设计为一个对象池只能存放一种对象,则对象池的实现实际就是固定内存块的池化管理,非常简单。毕竟,对象的数量非常有限。
3、第三个需要的是队列:
·如果可以预料到极限的处理能力,采用固定大小的环形队列来作为缓冲区是比较不错的。一个生产者一个消费者是常见的应用场景,环形队列有其经典的“锁无 关”算法,在一个线程读一个线程写的场景下,实现简单,性能还高,还不涉及资源的分配和释放。好啊,实在是好!
·涉及多个生产者消费者的时候,tbb::concurent_queue是不错的选择,线程安全,并发性也好,就是不知道资源的分配释放是否也管理得足 够好。
4、第四个需要的是映射表,或者说hash表:
·因为epoll是事件触发的,而一系列的流程可能是分散在多个事件中的,因此,必须保留下中间状态,使得下一个事件触发的时候,能够接着上次处理的位置 继续处理。要简单的话,STL的hash_map还行,不过得自己处理锁的问题,多线程环境下使用起来很麻烦。
·多线程环境下的hash表,最好的还是tbb::concurent_hash_map。
5、核心的线程是事件线程:
·事件线程是调用epoll_wait()等待事件的线程。例子代码里面,一个线程干了所有的事情,而需要开发一个高性能的服务器的时候,事件线程应该专 注于事件本身的处理,将触发事件的socket句柄放到对应的处理队列中去,由具体的处理线程负责具体的工作。
6、accept()单独一个线程:
·服务端的socket句柄(就是调用bind()和listen()的这个)最好在单独的一个线程里面做accept(),阻塞还是非阻塞都无所谓,相 比整个服务器的通讯,用户接入的动作只是很小一部分。而且,accept()不放在事件线程的循环里面,减少了判断。
7、接收线程单独一个:
·接收线程从发生EPOLLIN事件的队列中取出socket句柄,然后在这个句柄上调用recv接收数据,直到缓冲区没有数据为止。接收到的数据写入以 socket为键的hash表中,hash表中有一个自增长的缓冲区,保存了客户端发过来的数据。
·这样的处理方式适合于客户端发来的数据很小的应用,比如HTTP服务器之类;假设是文件上传的服务器,则接受线程会一直处理某个连接的海量数据,其他客 户端的数据处理产生了饥饿。所以,如果是文件上传服务器一类的场景,就不能这样设计。
8、发送线程单独一个:
·发送线程从发送队列获取需要发送数据的SOCKET句柄,在这些句柄上调用send()将数据发到客户端。队列中指保存了SOCKET句柄,具体的信息 还需要通过socket句柄在hash表中查找,定位到具体的对象。如同上面所讲,客户端信息的对象不但有一个变长的接收数据缓冲区,还有一个变长的发送 数据缓冲区。具体的工作线程发送数据的时候并不直接调用send()函数,而是将数据写到发送数据缓冲区,然后把SOCKET句柄放到发送线程队列。
·SOCKET句柄放到发送线程队列的另一种情况是:事件线程中发生了EPOLLOUT事件,说明TCP的发送缓冲区又有了可用的空间,这个时候可以把 SOCKET句柄放到发送线程队列,一边触发send()的调用;
·需要注意的是:发送线程发送大量数据的时候,当频繁调用send()直到TCP的发送缓冲区满后,便无法再发送了。这个时候如果循环等待,则其他用户的 发送工作受到影响;如果不继续发送,则EPOLL的ET模式可能不会再产生事件。解决这个问题的办法是在发送线程内再建立队列,或者在用户信息对象上设置 标志,等到线程空闲的时候,再去继续发送这些未发送完成的数据。
9、需要一个定时器线程:
·一位将epoll使用的高手说道:“单纯靠epoll来管理描述符不泄露几乎是不可能的。完全解决方案很简单,就是对每个fd设置超时时间,如果超过 timeout的时间,这个fd没有活跃过,就close掉”。
·所以,定时器线程定期轮训整个hash表,检查socket是否在规定的时间内未活动。未活动的SOCKET认为是超时,然后服务器主动关闭句柄,回收 资源。
10、多个工作线程:
·工作线程由接收线程去触发:每次接收线程收到数据后,将有数据的SOCKET句柄放入一个工作队列中;工作线程再从工作队列获取SOCKET句柄,查询 hash表,定位到用户信息对象,处理业务逻辑。
·工作线程如果需要发送数据,先把数据写入用户信息对象的发送缓冲区,然后把SOCKET句柄放到发送线程队列中去。
·对于任务队列,接收线程是生产者,多个工作线程是消费者;对于发送线程队列,多个工作线程是生产者,发送线程是消费者。在这里需要注意锁的问题,如果采 用tbb::concurrent_queue,会轻松很多。
11、仅仅只用scoket句柄作为hash表的键,并不够:
·假设这样一种情况:事件线程刚把某SOCKET因发生EPOLLIN事件放入了接收队列,可是随即客户端异常断开了,事件线程又因为EPOLLERR事 件删除了hash表中的这一项。假设接收队列很长,发生异常的SOCKET还在队列中,等到接收线程处理到这个SOCKET的时候,并不能通过 SOCKET句柄索引到hash表中的对象。
·索引不到的情况也好处理,难点就在于,这个SOCKET句柄立即被另一个客户端使用了,接入线程为这个SCOKET建立了hash表中的某个对象。此 时,句柄相同的两个SOCKET,其实已经是不同的两个客户端了。极端情况下,这种情况是可能发生的。
·解决的办法是,使用socket fd + sequence为hash表的键,sequence由接入线程在每次accept()后将一个整型值累加而得到。这样,就算SOCKET句柄被重用,也 不会发生问题了。
12、监控,需要考虑:
·框架中最容易出问题的是工作线程:工作线程的处理速度太慢,就会使得各个队列暴涨,最终导致服务器崩溃。因此必须要限制每个队列允许的最大大小,且需要 监视每个工作线程的处理时间,超过这个时间就应该采用某个办法结束掉工作线程。
对于linux socket与epoll配合相关的一些心得记录 2008-07-29 17:57
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
1、通过上面语句可以简单设置缓冲区大小,测试证明:跟epoll结合的时候只有当 单次发送的数据全被从缓冲区读完毕之后才会再次被触发,多次发送数据如果没有 读取完毕当缓冲区未满的时候数据不会丢失,会累加到后面。
2、 如果缓冲区未满,同一连接多次发送数据会多次收到EPOLLIN事件。 单次发送数据>socket缓冲区大小的数据数据会被阻塞分次发送,所以循环接收可 以用ENLIGE错误判断。
3、如果缓冲区满,新发送的数据不会触发epoll事件(也无异常),每次recv 都会为缓冲区腾出空间,只有当缓冲区空闲大小能够再次接收数据epollIN事件可 以再次被触发 接收时接收大小为0表示客户端断开(不可能有0数据包触发EPOLLIN),-1表示异 常,针对errorno进行判断可以确定是合理异常还是需要终止的异常,>0而不等于 缓冲区大小表示单次发送结束。
4、 如果中途临时调整接收缓存区大小,并且在上一次中数据没有完全接收到 用户空间,数据不会丢失,会累加在一起 所以总结起来,系统对于数据的完整性还是做了相当的保正,至于稳定性没有作更 深一步的测试
新增加:
5、如果主accept监听的soctet fd也设置为非阻塞,那么单纯靠epoll事件来驱 动的服务器模型会存在问题,并发压力下发现,每次accept只从系统中取得第一 个,所以如果恰冯多个 连接同时触发server fd的EPOLLIN事件,在返回的event数 组中体现不出来,会出现丢失事件的现象,所以当用ab等工具简单的压载就会发现 每次都会有最后几条信息得 不到处理,原因就在于此,我现在的解决办法是将 server fd的监听去掉,用一个线程阻塞监听,accept成功就处理检测client fd, 然后在主线程循环监听client事件,这样epoll在边缘模式下出错的概率就小,测 试表明效果明显
6、对于SIG部分信号还是要做屏蔽处理,不然对方socket中断等正常事件都会引起 整个服务的退出
7、sendfile(fd, f->SL->sendBuffer.inFd, (off_t *)&f->SL->sendBuffer.offset, size_need);注意sendfile函数的地三个变量是传 送地址,偏移量会自动增加,不需要手动再次增加,否则就会出现文件传送丢失现象
8、单线程epoll驱动模型误解:以前我一直认为单线程是无法处理web服务器这样 的有严重网络延迟的服务,但nginx等优秀服务器都是机遇事件驱动 模型,开始我 在些的时候也是担心这些问题,后来测试发现,当client socket设为非阻塞模式 的时候,从读取数据到解析http协议,到发送数据均在epoll的驱动下速度非常 快,没有必要采用多线程,我的单核 cpu(奔三)就可以达到 10000page/second,这在公网上是远远无法达到的一个数字(网络延迟更为严 重),所以单线程的数据处理能力已经很 高了,就不需要多线程了,所不同的是 你在架构服务器的时候需要将所有阻塞的部分拆分开来,当epoll通知你可以读取 的时候,实际上部分数据已经到了 socket缓冲区,你所读取用的事件是将数据从 内核空间拷贝到用户空间,同理,写也是一样的,所以epoll重要的地方就是将这 两个延时的部分做了类似 的异步处理,如果不需要处理更为复杂的业务,那单线 程足以满足1000M网卡的最高要求,这才是单线程的意义。 我以前构建的web服务器就没有理解epoll,采用epoll的边缘触发之程处后怕事件 丢失,或者单线理阻塞,所以自己用多线程构建了一个任务调度器, 所有收 到的事件统统压进任无调度器中,然后多任务处理,我还将read和write分别用两 个调度器处理,并打算如果中间需要特殊的耗时的处理就增加一套 调度器,用少量线程+epoll的方法来题高性能,后来发现read和write部分调度器是多余 的,epoll本来就是一个事件调度器,在后面再次缓存 事件分部处理还不如将 epoll设为水平模式,所以多此一举,但是这个调度起还是有用处的 上面讲到如果中间有耗时的工作,比如数据库读写,外部资源请求(文 件,socket)等这些操作就不能阻塞在主线程里面,所以我设计的这个任务调度器 就有 用了,在epoll能处理的事件驱动部分就借用epoll的,中间部分采用模块化 的设计,用函数指针达到面相对象语言中的“委托”的作用,就可以满足不同 的需 要将任务(fd标识)加入调度器,让多线程循环执行,如果中间再次遇到阻塞就会 再次加入自定义的阻塞器,检测完成就加入再次存入调度器,这样就可以将 多种 复杂的任务划分开来,相当于在处理的中间环节在自己购置一个类似于epoll的事 件驱动器
9、多系统兼容:我现在倒是觉得与其构建一个多操作系统都支持的服务器不 如构建特定系统的,如果想迁移再次改动,因为一旦兼顾到多个系统的化会大大增 加系 统的复杂度,并且不能最优性能,每个系统都有自己的独有的优化选项,所 以我觉得迁移的工作量远远小于兼顾的工作量
10模块化编程,虽然用c还是要讲求一些模块化的设计的,我现在才发现几乎面相 对想的语言所能实现的所有高级特性在c里面几乎都有对应的解决办法(暂时发现 除了操作符重载),所有学过高级面相对象的语言的朋友不放把模式用c来实现, 也是一种乐趣,便于维护和自己阅读
11、养成注释的好习惯
setsockopt -设置socket
1.closesocket(一般不会立即关闭而经历TIME_WAIT的过程)后想继续重用该socket:
BOOL bReuseaddr=TRUE;
setsockopt(s,SOL_SOCKET ,SO_REUSEADDR,(const char*)&bReuseaddr,sizeof(BOOL));
2. 如果要已经处于连接状态的soket在调用closesocket后强制关闭,不经历
TIME_WAIT的过程:
BOOL bDontLinger = FALSE;
setsockopt(s,SOL_SOCKET,SO_DONTLINGER,(const char*)&bDontLinger,sizeof(BOOL));
3.在send(),recv()过程中有时由于网络状况等原因,发收不能预期进行,而设置收发时限:
int nNetTimeout=1000;//1秒
//发送时限
setsockopt(socket,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
//接收时限
setsockopt(socket,SOL_S0CKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));
4.在send()的时候,返回的是实际发送出去的字节(同步)或发送到socket缓冲区的字节
(异步);系统默认的状态发送和接收一次为8688字节(约为8.5K);在实际的过程中发送数据
和接收数据量比较大,可以设置socket缓冲区,而避免了send(),recv()不断的循环收发:
// 接收缓冲区
int nRecvBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_RCVBUF,(const char*)&nRecvBuf,sizeof(int));
//发送缓冲区
int nSendBuf=32*1024;//设置为32K
setsockopt(s,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
5. 如果在发送数据的时,希望不经历由系统缓冲区到socket缓冲区的拷贝而影响
程序的性能:
int nZero=0;
setsockopt(socket,SOL_S0CKET,SO_SNDBUF,(char *)&nZero,sizeof(nZero));
6.同上在recv()完成上述功能(默认情况是将socket缓冲区的内容拷贝到系统缓冲区):
int nZero=0;
setsockopt(socket,SOL_S0CKET,SO_RCVBUF,(char *)&nZero,sizeof(int));
7.一般在发送UDP数据报的时候,希望该socket发送的数据具有广播特性:
BOOL bBroadcast=TRUE;
setsockopt(s,SOL_SOCKET,SO_BROADCAST,(const char*)&bBroadcast,sizeof(BOOL));
8.在client连接服务器过程中,如果处于非阻塞模式下的socket在connect()的过程中可
以设置connect()延时,直到accpet()被呼叫(本函数设置只有在非阻塞的过程中有显著的
作用,在阻塞的函数调用中作用不大)
BOOL bConditionalAccept=TRUE;
setsockopt(s,SOL_SOCKET,SO_CONDITIONAL_ACCEPT,(const char*)&bConditionalAccept,sizeof(BOOL));
如果在发送数据的过程中(send()没有完成,还有数据没发送)而调用了closesocket(),以前我们
一般采取的措施是"从容关闭"shutdown(s,SD_BOTH),但是数据是肯定丢失了,如何设置让程序满足具体
应用的要求(即让没发完的数据发送出去后在关闭socket)?
struct linger {
u_short l_onoff;
u_short l_linger;
};
linger m_sLinger;
m_sLinger.l_onoff=1;//(在closesocket()调用,但是还有数据没发送完毕的时候容许逗留)
// 如果m_sLinger.l_onoff=0;则功能和2.)作用相同;
m_sLinger.l_linger=5;//(容许逗留的时间为5秒)
setsockopt(s,SOL_SOCKET,SO_LINGER,(const char*)&m_sLinger,sizeof(linger));
/
线程于进程的好处在于:
方便通信,线程共享了代码与数据空间,所以对共享空间提供了最原始的支持
可以用线程运行完销毁的方式而不需要回收线程资源,只要进程退出,所有线程就销毁了,不需要担心有僵尸进程的出现,也就是资源不能回收的问题。
对于并发比较高的服务器,并且每个处理时间又不是太长的情况下,可以采用线程池的方式
在同等情况下,线程所占资源略少于进程,因为线程在访问一共享变量时,在物理内存中仅有一份此变量所占空间,若是进程间需要改写同一全局变量时,此时就会产生“写时复制”,会产生两份空间(对一个变量的改写,会造成多占用大于等于4K的物理空间)
进程于线程的好处在于:
不需要担心太多因为访问共享资源而造成的各种同步与互斥问题,如果需要共享某部分内容,需要走专用的进程间通信手段,也就是说对于共享空间是可控制的,不会出现随机性
不用担心一不小心就造成函数重入的问题
在不同的进程中,可以使用不同的ELF文件作为执行体,当然,在线程中也可以再进行fork+execv来实现这种方案
无论是线程还是进程,其调度方式是一样的
长连接,用poll/select/epoll做多路复用的方式优缺点:
由于不会造成多线程与多进程,所以所有代码都在一个执行体内,都在同一调度单元中,节省了资源的开销,如内存的占用,进程切换的开销。
由于所有处理都在同一个调度单元内,也就是多个连接共用一个进程的时间片,如果系统中还有很多其它优先级较高进程或者实时进程,平均下来每个连接所占用的 CPU时间就较少,且如果一个连接处于死循环中,若不加其它控制,其它连接就永远得不到响应,也就是说每个连接的响应实时性会受到其它连接的影响。
如果对于每个连接的处理方式不同,会造成代码的不好控制,因为会有太多的逻辑判断。
以上三种方式,各有优缺点,主要是楼主的需求,这三种方式并非是互斥的,可以交叉使用,灵活控制,从而最优化你的软件。当然,如果并发连接达到2000个 以上,并发处理也达到几百上千个以上(且每个处理过程会执行很长),那么推荐你采用分布式处理,单个PC机是无法承受这种负荷的(若使用专用服务器,性能 会好一些,这个相对限制会宽一些),至少会造成响应时间过长
、、、、、、、、、、、、、、、、、、、、、、、、、、、
设置套接口的选项。
#include <winsock.h>
int PASCAL FAR setsockopt( SOCKET s, int level, int optname,
const char FAR* optval, int optlen);
s:标识一个套接口的描述字。
level:选项定义的层次;目前仅支持SOL_SOCKET和IPPROTO_TCP层次。
optname:需设置的选项。
optval:指针,指向存放选项值的缓冲区。
optlen:optval缓冲区的长度。
注释:
setsockopt()函数用于任意类型、任意状态套接口的设置选项值。尽管在不同协议层上存在选项,但本函数仅定义了最高的“套接口”层次上的选项。选项影响套接口的操作,诸如加急数据是否在普通数据流中接收,广播数据是否可以从套接口发送等等。
有两种套接口的选项:一种是布尔型选项,允许或禁止一种特性;另一种是整形或结构选项。允许一个布尔型选项,则将optval指向非零整形数;禁止一个选 项optval指向一个等于零的整形数。对于布尔型选项,optlen应等于sizeof(int);对其他选项,optval指向包含所需选项的整形数 或结构,而optlen则为整形数或结构的长度。SO_LINGER选项用于控制下述情况的行动:套接口上有排队的待发送数据,且 closesocket()调用已执行。参见closesocket()函数中关于SO_LINGER选项对closesocket()语义的影响。应用 程序通过创建一个linger结构来设置相应的操作特性:
struct linger {
int l_onoff;
int l_linger;
};
为了允许SO_LINGER,应用程序应将l_onoff设为非零,将l_linger设为零或需要的超时值(以秒为单位),然后调用setsockopt()。为了允许SO_DONTLINGER(亦即禁止SO_LINGER),l_onoff应设为零,然后调用setsockopt()。
缺省条件下,一个套接口不能与一个已在使用中的本地地址捆绑(参见bind())。但有时会需要“重用”地址。因为每一个连接都由本地地址和远端地址的组 合唯一确定,所以只要远端地址不同,两个套接口与一个地址捆绑并无大碍。为了通知WINDOWS套接口实现不要因为一个地址已被一个套接口使用就不让它与 另一个套接口捆绑,应用程序可在bind()调用前先设置SO_REUSEADDR选项。请注意仅在bind()调用时该选项才被解释;故此无需(但也无 害)将一个不会共用地址的套接口设置该选项,或者在bind()对这个或其他套接口无影响情况下设置或清除这一选项。
一个应用程序可以通过打开SO_KEEPALIVE选项,使得WINDOWS套接口实现在TCP连接情况下允许使用“保持活动”包。一个WINDOWS套 接口实现并不是必需支持“保持活动”,但是如果支持的话,具体的语义将与实现有关,应遵守RFC1122“Internet主机要求-通讯层”中第 4.2.3.6节的规范。如果有关连接由于“保持活动”而失效,则进行中的任何对该套接口的调用都将以WSAENETRESET错误返回,后续的任何调用 将以WSAENOTCONN错误返回。
TCP_NODELAY选项禁止Nagle算法。Nagle算法通过将未确认的数据存入缓冲区直到蓄足一个包一起发送的方法,来减少主机发送的零碎小数据 包的数目。但对于某些应用来说,这种算法将降低系统性能。所以TCP_NODELAY可用来将此算法关闭。应用程序编写者只有在确切了解它的效果并确实需 要的情况下,才设置TCP_NODELAY选项,因为设置后对网络性能有明显的负面影响。TCP_NODELAY是唯一使用IPPROTO_TCP层的选 项,其他所有选项都使用SOL_SOCKET层。
如果设置了SO_DEBUG选项,WINDOWS套接口供应商被鼓励(但不是必需)提供输出相应的调试信息。但产生调试信息的机制以及调试信息的形式已超出本规范的讨论范围。
setsockopt()支持下列选项。其中“类型”表明optval所指数据的类型。
选项 类型 意义
SO_BROADCAST BOOL 允许套接口传送广播信息。
SO_DEBUG BOOL 记录调试信息。
SO_DONTLINER BOOL 不要因为数据未发送就阻塞关闭操作。设置本选项相当于将SO_LINGER的l_onoff元素置为零。
SO_DONTROUTE BOOL 禁止选径;直接传送。
SO_KEEPALIVE BOOL 发送“保持活动”包。
SO_LINGER struct linger FAR* 如关闭时有未发送数据,则逗留。
SO_OOBINLINE BOOL 在常规数据流中接收带外数据。
SO_RCVBUF int 为接收确定缓冲区大小。
SO_REUSEADDR BOOL 允许套接口和一个已在使用中的地址捆绑(参见bind())。
SO_SNDBUF int 指定发送缓冲区大小。
TCP_NODELAY BOOL 禁止发送合并的Nagle算法。
setsockopt()不支持的BSD选项有:
选项名 类型 意义
SO_ACCEPTCONN BOOL 套接口在监听。
SO_ERROR int 获取错误状态并清除。
SO_RCVLOWAT int 接收低级水印。
SO_RCVTIMEO int 接收超时。
SO_SNDLOWAT int 发送低级水印。
SO_SNDTIMEO int 发送超时。
SO_TYPE int 套接口类型。
IP_OPTIONS 在IP头中设置选项。
返回值:
若无错误发生,setsockopt()返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
错误代码:
WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
WSAENETDOWN:WINDOWS套接口实现检测到网络子系统失效。
WSAEFAULT:optval不是进程地址空间中的一个有效部分。
WSAEINPROGRESS:一个阻塞的WINDOWS套接口调用正在运行中。
WSAEINVAL:level值非法,或optval中的信息非法。
WSAENETRESET:当SO_KEEPALIVE设置后连接超时。
WSAENOPROTOOPT:未知或不支持选项。其中,SOCK_STREAM类型的套接口不支持SO_BROADCAST选项,SOCK_DGRAM 类型的套接口不支持SO_DONTLINGER 、SO_KEEPALIVE、SO_LINGER和SO_OOBINLINE选项。
WSAENOTCONN:当设置SO_KEEPALIVE后连接被复位。
WSAENOTSOCK:描述字不是一个套接口。
参见:
bind(), getsockopt(), ioctlsocket(), socket(), WSAAsyncSelect().