0 理解Socket
什么是Socket呢?
我们经常把Socket翻译为套接字,Socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。
socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种”打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个”文件”,在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。
套接字是通信的基石,可看作不同主机的进程进行双向通信的端点。
套接字有两种不同的类型:流式套接字和数据报套接字。
套接字可处于阻塞模式或非阻塞模式。
1 WinSock API
什么是WinSock呢?
WinSock是一套开放的、支持多种协议的Windows下网络编程的接口,是Windows网络编程实时上的标准。
Winsock版本:目前Winsock有两个版本,分别是WinSock1.1和WinSock2.0,使用方法如下:
WinSock1.1:
#include <winsock.h>
#pragma comment(lib, “wsock32.lib”)
WinSock2.0:
#include <winsock2.h>
#pragma comment(lib, “ws2_32.lib”)
WinSock服务是以动态链接库Winsock DLL形式实现的。
通用API函数列表
WinSock API 描述
WSAStartup Winsock启动
WSACleanup Winsock停止
WSASetLastError 错误的检查和控制
针对WinSock1.1存在的某些局限,WinSock2提供了许多方面的扩展(如支持多个传输协议的原始套接字、重叠IO模型、服务质量控制等)以支持功能更强大的应用,考虑兼容性,WinSock1.1的API都在WinSock2中保留了下来。
2 阻塞socket
基于TCP的套接字:
// 创建套接字
SOCKET socket(int af, int type, int protocol);
// 绑定地址端口
int bind(SOCKET s, const struct sockaddr *addr, socklen_t addrlen);
// 监听客户端
int listen(SOCKET s, int backlog);
// 接收客户端套接字请求
int accept(SOCKET s, struct sockaddr *addr, socklen_t *addrlen);
// 连接服务端套接字
int connect(SOCKET s, const struct sockaddr addr, socklen_t addrlen);
// 发送数据
int send(SOCKET s, const char FAR * buf, int len, int flags);
// 接收数据(阻塞)
int recv(SOCKET s, char buf, int len, int flags);
// 关闭套接字
int closesocket(SOCKET s);
基于UDP的套接字:
// 创建套接字
SOCKET socket(int af, int type, int protocol);
// 绑定地址端口
int bind(SOCKET s, const struct sockaddr addr, socklen_t addrlen);
// 发送数据
int sendto (SOCKET s, const char buf, int len, int flags, const struct sockaddr* to, int tolen);
// 接收数据(阻塞)
int recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr* from, int* fromlen);
// 关闭套接字
int closesocket(SOCKET s);
总结:
在阻塞模式下,在IO操作完成之前,执行操作的WinSock函数会一直等待下去,不会立即返回,这就意味着任意一个线程在某一时刻只能进行一个IO操作,而且应用程序很难同时通过多个建好连接的套接字进行通信。
可见,在默认情况下套接字为阻塞模式。
这种情况下一般采用多线程方式,在不同的线程中进行不同的连接处理来避免阻塞,但是多线程会增加系统开销,而且线程同步会增加复杂度。
3 非阻塞Socket
WinSock API默认为阻塞模式,但是其提供了非阻塞模式套接字,非阻塞模式套接字使用上不如阻塞模式套接字简单,存在一点的难度,但是只要排除了这些困难,它在功能上还是很强大的。
可以使用ioctlsocket将套接字设置为非阻塞模式套接字:
int PASCAL FAR ioctlsocket (
IN SOCKET s,
IN long cmd,
IN OUT u_long FAR *argp);
// If *argp = 0, blocking is enabled;
// If *argp != 0, non-blocking mode is enabled.
代码片段(基于TCP)
1 | SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); |
将一个套接字设置为非阻塞模式后,WinSock API调用会立即返回。大多数情况下,这些调用都会”失败”,并返回一个WSAEWOULDBLOCK错误表示请求的操作在调用期间没有时间完成。由于会不断地返回这个错误,所以程序员需要通过不断地检查函数返回码以判断一个套接字何时可供读写。
C++ Code
1 | While(true) |
4 套接字IO模型
套接字的阻塞模式和非阻塞模式都存在一定的缺点,会给编程带来一定的麻烦。为了免去这样的麻烦,WinSock提供了集中不同的套接字IO模型对IO进行管理,它们包括:
- select(选择)
- WSAAsyncSelect(异步选择)
- WSAEventSelect(事件选择)
- Overlaped(重叠)
- Completion port(完成端口)
4.1 套接字IO模型:select(选择)
select模式是WinSock中最常见的IO模型。通过调用select函数可以确定一个或多个套接字的状态,判断套接字上是否存在数据,或者能否向一个套接字写入数据。有如下好处:
1)、防止应用程序在套接字处于阻塞模式时,在一次IO操作后被阻塞;
2)、防止在套接字处于非阻塞模式中时产生WSAEWOULDBLOCK错误。
函数原型:
1 | The **select** function determines the status of one or more sockets, waiting if necessary, to perform synchronous I/O. |
使用该模型时,在服务端(select主要用在服务端处理多个客户请求上)我们可以开辟两个线程,一个线程用来监听客户端的连接请求,另一个用来处理客户端的请求。就这样不需要一个一个客户请求对应一个服务器处理线程,减少了线程的开销。
select允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或多个时间发生或经历一段指定时间后才唤醒它。select告诉内核对哪些描述子感兴趣以及等待多长时间。这就是所谓的非阻塞模型,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高。
select本身是会阻塞的,我们可以使用select实现阻塞式套接字(如上),也可以实现异步套接字。我个人对实现异步套接字的理解是:你可以单独使用一个线程来进行你select,也就是说select阻塞你单独的线程,说白了就是让线程来完成异步。
该模型有个最大的缺点就是,它需要一个死循环不停的去遍历所有的客户端套接字集合,询问是否有数据到来,这样,如果连接的客户端很多,势必会影响处理客户端请求的效率,但它的优点就是解决了每一个客户端都去开辟新的线程与其通信的问题。
4.2 套接字IO模型:WSAAsyncSelect(异步选择)
如果有一个模型,可以不用去轮询客户端套接字集合,而是等待系统通知,当有客户端数据到来时,系统自动的通知我们的程序,这就解决了select模型带来的问题了。
于是WSAAsyncSelect模型登场了,WSAAsyncSelect模型就是这样一个解决了普通select模型问题的socket编程模型。它是在有客户端数据到来时,系统发送消息给我们的程序,我们的程序只要定义好消息的处理方法就可以了,用到的函数只要是WSAAsyncSelect。
1 | The WSAAsyncSelect function requests Windows message-based notification of network events for a socket. |
WSAAsyncSelect模型将套接字和Windows消息机制很好地粘合在一起,为用户异步SOCKET应用提供了一种较优雅的解决方案。
WSAAsyncSelect模型是非常简单的模型,它解决了普通select模型的问题,但是它最大的缺点就是它只能用在Windows程序上,因为它需要一个接收系统消息的窗口句柄,那么有没有一个模型既可以解决select模型的问题,又不限定只能是Windows程序才能用呢?请看下节。
4.3 套接字IO模型:WSAEventSelect(事件选择)
WSAEventSelect模型是一个不用主动去轮询所有客户端套接字是否有数据到来的模型,它也是在客户端有数据到来时,系统发送通知给我们的程序,但是,它不是发送消息,而是通过事件的方式来通知我们的程序,这就解决了WSAEventSelect模型只能用在Windows程序的问题。
该模型的实现,我们也可以开辟两个线程来进行处理,一个用来接收客户端的连接请求,一个用来与客户端进行通信,用到的主要函数有:WSAEventSelect,WSAWaitForMultipleEvents,WSAEnumNetworkEvents。
1 | The WSACreateEvent function creates a new event object. |
代码片段(接受客户端请求并注册事件)
1 | // 全局变量 |
代码片段(接受到事件并处理)
1 | DWORD WINAPI WorkerThread(LPVOID lpParam) |
该模型通过一个死循环里面调用WSAWaitForMultipleEvents函数来等待客户端套接字对应的Event的到来,一旦事件通知到达,就通过该套接字去接收数据。虽然WsaEventSelect模型的实现较前两种方法复杂,但它在效率和兼容性方面是最好的。
4.4 套接字IO模型:Overlaped(重叠)
以上三种模型虽然在效率方面有了不少的提升,但它们都存在一个问题,就是都预设了只能接收64个客户端连接,虽然我们在实现时可以不受这个限制,但是那样,它们所带来的效率提升又将打折扣,那又有没有什么模型可以解决这个问题呢?
当然有,它就是Overlaped模型。
优点:
1、可以运行在支持Winsock2的所有Windows平台 ,而不像完成端口只是支持NT系统。
2、比起阻塞、非阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,Overlapped I/O模型使应用程序能达到更佳的系统性能。
因为它和这5种模型不同的是:使用重叠模型的应用程序通知缓冲区收发系统直接使用数据,也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。
而这5种模型种,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区,差别就体现出来了。
3、从《Windows网络编程》中提供的试验结果中可以看到,在使用了P4 1.7G Xero处理器(CPU很强啊)以及768MB的回应服务器中,最大可以处理4万多个SOCKET连接,在处理1万2千个连接的时候CPU占用率才40% 左右(非常好的性能,已经直逼完成端口了^_^),再也不被限制在64个客户端连接数了,而且性能杠杠的!
原理:
概括一点说,重叠模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。针对这些提交的请求,在它们完成之后,应用程序会收到通知,于是就可以通过自己另外的代码来处理这些数据了。
需要注意的是,有两个方法可以用来管理重叠IO请求的完成情况(就是说接到重叠操作完成的通知):
1、事件对象通知(event object notification)
2、完成例程(completion routines),注意,这里并不是完成端口
我们知道WinSock2扩展中支持重叠IO模型,既然要使用重叠结构,我们常用的send、sendto、recv、recvfrom也都要被WSASend、WSASendto、WSARecv、WSARecvFrom替换掉了,这里只需要注意一点,它们的参数中都有一个Overlapped参数,我们可以假设是把我们的WSARecv这样的操作操作”绑定”到这个重叠结构上,提交一个请求,其他的事情就交给重叠结构去操心,而其中重叠结构又要与Windows的事件对象”绑定”在一起,这样我们调用完WSARecv以后就可以”坐享其成”,等到重叠操作完成以后,自然会有与之对应的事件来通知我们操作完成,然后我们就可以来根据重叠操作的结果取得我们想要德数据了。
WinSock重叠IO的基础是Windows的重叠IO机制。
1 | BOOL WINAPI ReadFile( |
如果我们在CreateFile的时候没有使用FILE_FLAG_OVERLAPPED标志,同时在调用ReadFile的时候把lpOverlapped这个参数设置的是null,那么ReadFile这个函数的调用一直要到读取完数据指定的数据后才会返回,如果没读取完,就会阻塞在这里。同样 ,writefile和ReadFile都是这样的。这样在读写大文件的时候,我们很多时间都浪费在等待ReadFile和writefile的返回上面。如果ReadFile和WriteFile是往管道里读写数据,那么有可能阻塞得更久,导致程序性能下降。为了解决这个问题,windows引进了重叠io的概念,同样是上面的ReadFile和WriteFile,如果在CreateFile的时候设置了file_flag_overlapped ,那么在调用ReadFile和WriteFile的时候就可以给他们最后一个参数传递一个overlapped结构。这样ReadFile或者WriteFile的调用马上就会返回,这时候你可以去做你要做的事,系统会自动替你完成ReadFile或者WriteFile,在你调用了ReadFile或者WriteFile后,你继续做你的事,系统同时也帮你完成ReadFile或WriteFile的操作,这就是所谓的重叠。使用重叠io还有一个好处,就是你可以同时发出几个ReadFile或者WriteFile的调用,然后用WaitForSingleObject或者WaitForMultipleObjects来等待操作系统的操作完成通知,在得到通知信号后,就可以用GetOverlappedResult来查询IO调用的结果。
如果我们在CreateFile的时候没有使用FILE_FLAG_OVERLAPPED标志,同时在调用ReadFile的时候把lpOverlapped这个参数设置的是null,那么ReadFile这个函数的调用一直要到读取完数据指定的数据后才会返回,如果没读取完,就会阻塞在这里。同样 ,writefile和ReadFile都是这样的。这样在读写大文件的时候,我们很多时间都浪费在等待ReadFile和writefile的返回上面。如果ReadFile和WriteFile是往管道里读写数据,那么有可能阻塞得更久,导致程序性能下降。为了解决这个问题,windows引进了重叠io的概念,同样是上面的ReadFile和WriteFile,如果在CreateFile的时候设置了file_flag_overlapped ,那么在调用ReadFile和WriteFile的时候就可以给他们最后一个参数传递一个overlapped结构。这样ReadFile或者WriteFile的调用马上就会返回,这时候你可以去做你要做的事,系统会自动替你完成ReadFile或者WriteFile,在你调用了ReadFile或者WriteFile后,你继续做你的事,系统同时也帮你完成ReadFile或WriteFile的操作,这就是所谓的重叠。使用重叠io还有一个好处,就是你可以同时发出几个ReadFile或者WriteFile的调用,然后用WaitForSingleObject或者WaitForMultipleObjects来等待操作系统的操作完成通知,在得到通知信号后,就可以用GetOverlappedResult来查询IO调用的结果。
举个例子:
你想当你有这样一个请求,就是
readfile(…) //1
writefile(…) //2
readfile(…) //3
你在程序中如果使用同步的话,那只有当你完成1以后2才会继续执行,2执行完以后3才会继续执行,这就是同步。
当如果使用异步的话,当系统遇到1时,ok,开一线程给它去完成该io请求,然后系统继续运行2,3,分别开两线程。 1-2-3如果是比较耗时的操作,尤其是运用在网络上,那么1-2-3这三个io请求是并行的,也就是重叠的。
4.4.1 基于事件通知的重叠I/O模型
The WSARecv function receives data from a connected socket.
int WSARecv(
__in SOCKET s,
__in_out LPWSABUF lpBuffers,
__in DWORD dwBufferCount,
__out LPDWORD lpNumberOfBytesRecvd,
__in_out LPDWORD lpFlags,
__in LPWSAOVERLAPPED lpOverlapped,
__in LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
C++ Code
1 | while(1) |
4.4.2 基于完成例程的重叠I/O模型
完成例程(Completion Routine)并非是大家所常听到的”完成端口”(Completion Port),而是另外一种管理重叠I/O请求的方式。
如果你想要使用重叠I/O机制带来的高性能模型,又懊恼于基于事件通知的重叠模型要收到64个等待事件的限制,还有点畏惧完成端口稍显复杂的初始化过程,那么”完成例程”无疑是你最好的选择!^_^因为完成例程摆脱了事件通知的限制,可以连入任意数量客户端而不用另开线程,也就是说只用很简单的一些代码就可以利用Windows内部的I/O机制来获得网络服务器的高性能。
而且个人感觉”完成例程”的方式比重叠I/O更好理解,因为就和我们传统的”回调函数”是一样的,也更容易使用一些,推荐!
基于事件通知的重叠I/O模型,在你投递了一个请求以后(比如WSARecv),系统在完成以后是用事件来通知你的,而在完成例程中,系统在网络操作完成以后会自动调用你提供的回调函数,区别仅此而已,是不是很简单呢?
采用完成例程的服务端,通信流程是这样的:
从图中可以看到,服务器端存在一个明显的异步过程,也就是说我们把客户端连入的SOCKET与一个重叠结构绑定之后,便可以将通讯过程全权交给系统内部自己去帮我们调度处理了(该过程见途中灰色部分),我们在主线程中就可以去做其他的事情,边等候系统完成的通知(调用事前注册的完成例程回调函数)就OK,这也就是完成例程高性能的原因所在。
有趣的比方:完成例程的处理过程,也就像我们告诉系统,说”我想要在网络上接收网络数据,你去帮我办一下”(投递WSARecv操作),”不过我并不知道网络数据合适到达,总之在接收到网络数据之后,你直接就调用我给你的这个函数(比如_CompletionProess),把他们保存到内存中或是显示到界面中等等,全权交给你处理了”,于是乎,系统在接收到网络数据之后,一方面系统会给我们一个通知,另外同时系统也会自动调用我们事先准备好的回调函数,就不需要我们自己操心了。
完成例程回调函数原型及传递方式:
1 | Void CALLBACK _CompletionRoutineFunc( |
因为我们需要给系统提供一个如上面定义的那样的回调函数,以便系统在完成了网络操作后自动调用,这里就需要提一下究竟是如何把这个函数与系统内部绑定的呢?如下所示,在WSARecv函数中是这样绑定的:最后一个参数
因为我们需要给系统提供一个如上面定义的那样的回调函数,以便系统在完成了网络操作后自动调用,这里就需要提一下究竟是如何把这个函数与系统内部绑定的呢?如下所示,在WSARecv函数中是这样绑定的:最后一个参数
1 | The WSARecv function receives data from a connected socket. |
小结:
重叠模型的缺点:它为每一个IO请求都开了一个线程,当同时有1000个请求发生,那么系统处理线程上下文[context]切换也是非常耗时的,所以这也就引发了完成端口模型iocp,用线程池来解决这个问题,这是下节要学习的内容。
4.5 套接字IO模型:Completion port(完成端口)
IOCP(I/O Completion Port,I/O完成端口)是性能最好的一种I/O模型。
它是应用程序使用线程池处理异步I/O请求的一种机制。在处理多个并发的异步I/O请求时,以往的模型都是在接收请求是创建一个线程来应答请求。这样就有很多的线程并行地运行在系统中。而这些线程都是可运行的,Windows内核花费大量的时间在进行线程的上下文切换,并没有多少时间花在线程运行上。再加上创建新线程的开销比较大,所以造成了效率的低下。
Windows Sockets应用程序在调用WSARecv()函数后立即返回,线程继续运行。当系统接收数据完成后,向完成端口发送通知包(这个过程对应用程序不可见)。
应用程序在发起接收数据操作后,在完成端口上等待操作结果。当接收到I/O操作完成的通知后,应用程序对数据进行处理。
完成端口其实就是上面两项的联合使用基础上进行了一定的改进。
一个完成端口其实就是一个通知队列,由操作系统把已经完成的重叠I/O请求的通知放入其中。当某项I/O操作一旦完成,某个可以对该操作结果进行处理的工作者线程就会收到一则通知。而套接字在被创建后,可以在任何时候与某个完成端口进行关联。
众所皆知,完成端口是在Windows平台下效率最高,扩展性最好的IO模型,特别针对于WinSock的海量连接时,更能显示出其威力。其实建立一个完成端口的服务器也很简单,只要注意几个函数,了解一下关键的步骤也就行了。
从本质上说,完成端口模型要求我们创建一个Win32完成端口对象(内核对象),通过指定数量的线程对重叠I/O请求进行管理,以便为已经完成的重叠I/O请求提供服务。要注意的是,所谓”完成端口”,实际是Win32、Windows NT以及Windows 2000采用的一种I/O构造机制,除套接字句柄之外,实际上还可接受其他东西。然而,本文只打算讲述如何使用套接字句柄,来发挥完成端口模型的巨大威力。使用这种模型之前,首先要创建一个I/O完成端口对象,用它面向任意数量的套接字句柄。管理多个I/O请求。要做到这—点,需要调用CreateIoCompletionPort函数。该函数定义如下:
1 | HANDLE CreateIoCompletionPort( |
5 原始套接字
一般情况下程序设计人员主要接触以下两类套接字:
流式套接字(SOCK_STREAM): 面向连接的套接字,对应于 TCP 应用程序。
数据包套接字(SOCK_DGRAM): 无连接的套接字,对应于UDP 应用程序。
这一类套接字为标准套接字。此外,还有一类原始套接字,它是一种对原始网络报文进行处理的套接字。原始套接字的用途主要有:
发送自定义的IP 数据报
发送ICMP 数据报
网卡的侦听模式,监听网络上的数据包。
伪装IP地址。
自定义协议的实现。
原始套接字主要应用在底层网络编程上,同时也是网络黑客的必备手段。eg:sniffer、拒绝服务(DoS)、IP 地址欺骗等都需要在原始套接字的基础上实现。
原始套接字与标准套接字之间的关系如下图所示。标准套接字与网络协议栈的TCP、UDP 层打交道,而原始套接字则与IP层级网络协议栈核心打交道。
网络监听技术很大程度上依赖于SOCKET_RAW。
要使用原始套接字,必须经过创建原始套接字、设置套接字选项和创建并填充相应协议头这三个步骤,然后用send、WSASend函数将组装好的数据发送出去。接收的过程也很相似,只是需要用recv或WSARecv函数接收数据。
1 | SOCKET sock; |
raw socket(原始套接字)工作原理与规则
https://blog.csdn.net/bcbobo21cn/article/details/51330174
raw socket(原始套接字)工作原理与规则
https://blog.csdn.net/bcbobo21cn/article/details/51330174