本文主要分析了几种Socket编程的模式。主要包括基本的阻塞Socket、非阻塞Socket、I/O多路复用。其中,阻塞和非阻塞是相对于套接字来讲的,而其余的模式本质上来讲是基于Socket的并发模式。I/O多路复用又主要分析了分析linux和windows下的经常使用模型。最后,比较这几种Socket编程模式的优缺点,并讨论多线程与Socket的组合使用和服务器开发的经常使用模式。linux
阻塞模式是最基本的Socket编程模式,在各类关于网络编程的书籍中都是入门的例子。就像其名所说,阻塞模式的Socket会阻塞当前的线程,直到结果返回,不然会一直等待。编程
非阻塞模式是相对阻塞模式来讲,Socket并不会阻塞当前线程,非阻塞模式不会等到结果返回,而会当即运行下去。windows
//设置套接字为非阻塞模式
fcntl( sockfd, F_SETFL, O_NONBLOCK); //O_NONBLOCK标志设置非阻塞模式
这里须要注意,阻塞/非阻塞、同步/异步以前的区别。在本质上它们是不一样的。同步和异步是相对操做结果来讲,会不会等待结果结果返回。而阻塞和非阻塞是相对线程是否被阻塞来讲的。其实,这二者存在本质的区别,它们的修饰对象是不一样的。阻塞和非阻塞是指进程访问的数据若是还没有就绪,进程是否须要等待,简单说这至关于函数内部的实现区别,也就是未就绪时是直接返回仍是等待就绪。而同步和异步是指访问数据的机制,同步通常指主动请求并等待I/O操做完毕的方式,当数据就绪后在读写的时候必须阻塞,异步则指主动请求数据后即可以继续处理其它任务,随后等待I/O,操做完毕的通知,这可使进程在数据读写时也不阻塞。由于二者在表现上常常相同,因此常常被混淆。服务器
I/O多路复用是一种并发服务器开发技术(处理多个客户端的链接)。经过该技术,系统内核缓冲I/O数据,当某个I/O准备好后,系统通知应用程序该I/O可读或可写,这样应用程序能够立刻完成相应的I/O操做,而不须要等待系统完成相应I/O操做,从而应用程序没必要因等待I/O操做而阻塞。
在linux下主要有select、poll、epoll三种模型,在freeBSD下则有kqueue,windwos下select、事件选择模型、重叠I/O和完成端口等。网络
select本质是经过设置或检查存放fd标志位的数据结构来进行下一步的处理。select是采用轮询fd集合来进行处理的。数据结构
//select相关函数 int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,const struct timeval *timeout) //返回值:就绪描述符的数目,超时返回0,出错返回-1 void FD_ZERO(fd_set *fdset); //清空集合 void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中 void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除 int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否能够读写
可是,select存在必定的缺陷。单个进程可监视的fd数量被限制,linux下通常为1024。虽然是能够修改的,可是老是有限制的。在每次调用select时,都须要把fd集合从用户态拷贝到内核态,并且须要循环整个fd集合,这个开销不少时候是比较大的。多线程
poll的实现和select很是类似,本质上是相同,只是描述fd集合的方式不一样。poll是基于链表来存储的。这虽然没有了最大链接数的限制,可是仍然还有fd集合拷贝和循环带来的开销。并且poll还有一个特色是水平触发,内核通知了fd后,没有被处理,那么内核就会不断的通知,直到被处理。并发
//poll相关函数 int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
epoll是对select和poll的改进。相较于poll,epoll使用“事件”的就绪通知,经过epoll_ctl注册fd,一旦该fd就绪,内核就会采用相似callback的回调机制来激活该fd,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程,这样不在须要轮询,判断fd合计合集是否为空。并且epoll不只支持水平触发,还支持边缘触发。边缘触发是指内核通知fd以后,无论处不处理都不在通知了。在存储fd的集合上,epoll也采用了更为优秀的mmap,并且会保证fd集合拷贝只会发生一次。app
//epoll相关函数 int epoll_create(int size); //句柄的建立 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //事件注册 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); //等待事件的发生
事件选择模型是基于消息的。它容许程序经过Socket,接收以事件为基础的网络事件通知。异步
//事件选择模型相关函数 WSAEVENT WSACreatEvent(void); //建立事件对象 int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents); //关联事件
重叠I/O模型是异步I/O模型。重叠模型的核心是一个重叠数据结构。重叠模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。若想以重叠方式使用文件,必须用FILE_FLAG_OVERLAPPED 标志打开它。当I/O操做完成后,系统通知应用程序。利用重叠I/O模型,应用程序在调用I/O函数以后,只须要等待I/O操做完成的消息便可。
HANDLE hFile = CreateFile(lpFileName, GENERIC_READ |
GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
IOCP完成端口是目前Windows下性能最好的I/O模型,固然也是最复杂的。简单的说,IOCP 是一种高性能的I/O模型,是一种应用程序使用线程池处理异步I/O请求的机制。IOCP将全部用户的请求都投递到一个消息队列中去,而后线程池中的线程逐一从消息队列中去取出消息并加以处理,就能够避免针对每个I/O请求都开线程。不只减小了线程的资源,也提升了线程的利用率。
//IOCP简单流程 //建立完成端口 Port port = createIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, fixedThreadCount()); //将Socket关联到IOCP CreateIoCompletionPort((HANDLE )m_sockClient,m_hIocp, (ULONG_PTR )m_sockClient, 0); //投递AcceptEx请求 LPFN_ACCEPTEX m_lpfnAcceptEx; // AcceptEx函数指针 GUID GuidAcceptEx = WSAID_ACCEPTEX; // GUID,这个是识别AcceptEx函数必须的 DWORD dwBytes = 0; WSAIoctl( m_pListenContext->m_Socket, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidAcceptEx, sizeof(GuidAcceptEx), &m_lpfnAcceptEx, sizeof(m_lpfnAcceptEx), &dwBytes, NULL, NULL); //使用GetQueuedCompletionStatus()监控完成端口 void *lpContext = NULL; OVERLAPPED *pOverlapped = NULL; DWORD dwBytesTransfered = 0; BOOL bReturn = GetQueuedCompletionStatus( pIOCPModel->m_hIOCompletionPort, &dwBytesTransfered, (LPDWORD)&lpContext, &pOverlapped, INFINITE ); //收到通知 int nBytesRecv = WSARecv(pIoContext->m_Socket, pIoContext ->p_wbuf, 1, &dwBytes, 0, pIoContext->p_ol, NULL);
在以上I/O复用模型的讨论中,其实都含有线程的使用。重叠I/O和I/O完成端口都是利用了线程。这也能够看出在高并发服务器的开发中,采用线程也是十分必要的。在I/O完成端口的使用中,还会使用到线程池,这也是如今应用十分普遍的。经过线程池,能够下降频繁建立线程带来的开销。
在Windows下通常使用windows提供I/O模型就足够应付不少场景。可是,在linux下I/O模型都是和线程不相关的。有时为了更高的性能,也会采起线程池和I/O复用模型结合使用。好比许多Linux服务端程序就采用epoll和线程池结合的形式,固然引入线程也带来了更多的复杂度,须要注意线程的控制和性能开销(线程的主要开销在线程的切换上)。而epoll原本也足够优秀,因此仅用epoll也是能够的,像libevent这种著名的网络库也是采用epoll实现的。固然,在linux下也有只使用多进程或多线程来达到并发的。这样会带来必定缺点,程序须要维护大量的Scoket。在服务端开发中使用线程,也要劲量保证无锁,锁也是很高的开销的。