一.套接字Socket基本概念
WinSock(Windows Sockets)是处理网络通信的Windows API。许多函数与在BSD中使用的Berkely套接字函数是相同的。套接字,简单的说就是通信双方的一种约定,用套接字中的相关函数来完成通信过程。
应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口。
区分不同应用程序进程间的网络通信和连接,主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。Socket原意是“插座”,通过将这3个参数结合起来,与一个“插座”Socket绑定,应用层就可以和传输层通过套接字接口区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
每一个基于TCP/IP网络通讯的程序(进程)都被赋予了唯一的端口(号),端口是一个信息缓冲区,用于保留Socket中的输入/输出信息,端口号是一个16位无符号整数(unsigned short),范围是0-65535,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于1024的端口号保留给标准应用程序,比如pop3的端口号就是110。每一个套接字都组合进了IP地址、端口、端口号,这样形成的整体就可以区别每一个套接字。
Socket可以看成在两个程序进行通讯连接中的一个端点(endpoint),一个程序将一段信息写入Socket中,该Socket将这段信息发送给另外一个Socket中,使这段信息能传送到其他程序中。一般一个server服务器对应很多客户端client连接,服务器必须维护一张客户连接列表,每增加一个客户端连接服务器端都要新建一个套接字负责与新增客户端进行对话通信。
传输套接字主要有两类:流式套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)。流类型的套接字是为需要可靠连接的应用程序设计的。这些程序通常使用连续的数据流。用于这种类型套接字的协议是TCP,适合FTP这类实现。流套接字是最常用的,一些众所周知的协议如HTTP、TCP、SMTP、POP3等都是基于面向流的协议。
数据报套接字使用UDP做为下层协议,是无连接的,有一个最大缓冲区大小(数据包大小的最大值)。它是为那些需要发送小数据包,并且对可靠性要求不高的应用程序设计的。与流式套接字不同,数据报套接字并不保证数据会到达终端,也不保证它是以正确的顺序到来的。数据报套接字的传输效率相当高,它经常用于音频或视频应用程序。对这些程序来说,速度比可靠性更加重要。
二.基于套接字的C/S通信模式
1. 基于Socket的C/S通信模式
2. 关于监听套接字和服务套接字的区别
一个连接由(server_ip, server_port)和(client_ip, client_port)唯一确定。你可以调用 getsockname() 函数获取与某个套接字关联的本地地址,调用 getpeername() 函数获取与某个套接字关联的目的地址。
关于监听套接字和 accept() 返回的新的套接字的区别,借用以下情景说明。好比你去吃饭,饭店迎宾小姐(监听 SOCKET)看到你来后和你打招呼,然后(ACCEPT)找来一个服务员(NEW SOCKET)伺候你,然后继续守在门口欢迎(监听)下一个。当然,迎宾小姐会记录一下哪一位服务员小姐招待了你那一桌;如再有新客人来,迎宾小姐(同一监听 SOCKET)又会安排另一位服务员(NEW SOCKET)伺候。迎宾小姐走了,伺候每一位客人的服务员不受影响。从以上情景可知连接建立后,客户端用发出连接的那个 SOCKET 向服务器发数据,是发给服务器新创建的 SOCKET,而不是服务器的监听 SOCKET。服务器的监听 SOCKET 永远只是用来接受连接请求。当然,一个酒店可能有前后或多个暗门,相应可能有多个迎宾小姐。服务器也可以在多个服务端口上监听受理不同的业务连接。
三.套接字的两种模式
套接字有阻塞(锁定)和非阻塞(非锁定)两种模式。非阻塞和阻塞描述的是 WinSock API 调用行为特性。
1. 阻塞模式套接字
在一个阻塞套接字上调用任何 WinSock API 函数(accept()/recv()/send()),都会耗费或长或短的时间“等待”返回,这会阻塞调用线程。直到有点数据可读或可写时,这种调用才返回。
阻塞模式的优点是符合时序,容易入手。缺点也是显而易见的,假设这样一种情景,一个线程中要处理多个客户(套接字A、B),我们在套接字A、B上顺序调用recv企图依次接收数据。若客户A始终不发数据,则recv(A)函数将可能永远无法返回,在这期间我们错失了客户B发送来的数据。
大多数WinSock应用都遵循“生产者-消费者”模型,应用程序需要读取(或写入)指定数量的字节,然后再对读取的数据执行一些计算。在应用程序中,可以为每个套接字都创建一个负责读取网络数据的读线程(ReadThread),以及一个负责对数据执行计算的数据处理线程(ProcessThread)。尽管这会增大一些开销,但的确是一种可行的方案。缺点便是扩展性极差,而且无法应对大规模的通信情况。
2. 非阻塞模式套接字
将一个套接字置为非阻塞模式之后,WinSock API 调用会立即返回。大多数情况下,这些调用都会“失败”(SOCKET_ERROR),并返回一个WSAEWOULDBLOCK错误。它意味着请求的操作在调用期间没有时间完成。举个例子来说,假如在系统的输入缓冲区中,尚不存在“待决”的数据,那么recv()调用就会返回WSAEWOULDBLOCK错误。通常,我们需要重复调用同一个函数(轮询),直至获得一个成功返回代码。
假如需要编写更多的代码,以便在每个 Winsock调用中,对收到一个WSAEWOULDBLOCK错误的可能性加以应付,那么非阻塞套接字便显得有些难于操作。在这些情况下,可考虑使用“套接字I/O模型”,它管理I/O完成通知,帮助应用程序判断套接字何时可供读写。
四.同步I/O和异步I/O
1. I/O 模型
套接字模式讨论的是套接字函数的调用特性,而I/O模型讨论的是调用背后的行为特性。
当CPU执行代码(当前活动线程)时遇上一个I/O请求(例如调用ReadFile()/WriteFile()或recv()/send())时,系统产生一个中断,当前活动线程阻塞在此,让CPU去完成这个I/O请求,等到完成后,系统再次产生一个中断让原先的程序继续运行。也就说通过中断保持这两者间的同步,可以将中断理解为硬件化的信号量。
这就是所谓的同步I/O,一个线程中只可能同时处理一个I/O请求。因为一个I/O操作是非常耗时的,所以代码挂起后等待I/O完成的这段时间内,这个线程浪费了很多个指令周期。如果要同时反复读写大文件,则同步I/O的效率是很低的。
当然,可以考虑使用多线程来处理。例如在设计服务器时可以使用多线程来处理客户请求,每有一个客户连接请求,就创建一个新线程,专门处理它的通信请求。对于小型服务器来说,这不是问题。对于同时处理成千上万个请求的大型服务器而言,使用多线程是无效的,因为系统能够支持的线程数量毕竟是有限制的。另外一种解决方案就是使用共享负载的线程池,这涉及到异步I/O。
在异步I/O中,当CPU执行你的代码遇上一个I/O请求时,使用一个线程去处理I/O请求,并且当前调用线程并不挂起。如果后续代码和这个I/O有关(例如需要等待I/O操作的结果),那么它就要等到这个I/O操作完成。通常在一个线程中调用WaitForSingleObject/WaitForMultipleObjects、WSAWaitForMultipleEvents、(WSA)GetOverlappedResult等函数就可以得到I/O完成的消息,然后再对数据进行处理。但如果后续的代码和这个I/O操作无关,你就可以以更快的速度执行下去了,而无需等待I/O请求的完成了,这就是异步I/O了。
2. 同步和异步
关于同步和异步,参考以下实例:
举例1:手机的通讯
手机打电话时同步,发短信是异步。
举例2:请你吃饭
同步就是你叫我去吃饭,我听到了就和你去吃饭;如果没有听到,你就不停的叫,直到我告诉你听到了,才一起去吃饭。
异步就是你叫我,然后自己去吃饭,我得到消息后可能立即走,也可能等到下班才去吃饭。
举例3:Windows消息处理
SendMessage同步:把一条消息投放到创建hWnd窗口的线程的消息队列中,直到消息被处理完毕才返回。
PostMessage异步:把一条消息投放到创建hWnd窗口的线程的消息队列中,不等消息被处理就立即返回。
举例4:B/S交互
普通B/S模式同步:提交请求→等待服务器处理(这期间客户浏览器不能干任何其他事情)→处理完毕返回。
AJAX技术异步:请求通过事件触发→服务器处理(这期间浏览器仍然可以作其他事情)→处理完毕。
参考:
《Windows 2000 Systems Programming Black Book》 Al Williams
《Network Programming for Microsoft Windows》 Anthony Jones,Jim Ohlund
《同步和异步的区别》