socket编程与线程模型四
3、无连接socket与多线程
无连接socket很灵活,可以通过同一个socket向很多个地址进行数据写入,从同一个地址进行数据读取。所以这种服务器的组织形式也会很灵活。比如,利用多线程共享同一个服务器端的socket,进行数据读取和写入。
但是需要注意,socket是特殊的I/O,既然属于I/O,那么线程同步与互斥是非常重要的。因为它们读写socket的顺序将不能被保证,或者无法预料。理论上一个端口号对应于不同的缓冲区,也就是端口号是tcp/ip协议栈上数据缓冲区的句柄。
五、有连接的socket
1、概述
有连接的socket,其编程方法与无连接的客户端和服务器端有很大差别。
面向连接的socket需要说明的是,面向连接的socket变成模型中,服务器端创建一个socket,并把一个地址与这个socket显式绑扎。
面向连接的socket二
为了详细的了解面向连接的socket,我们从accept()开始。
accept()从指定的socket的连接请求等待队列里面取出第一个连接请求,然后它就返回新建的一个socket句柄。这个新建的socket可以完成本次取出的连接请求,并开始为它服务。这个被新建的socket具备和用于监听连接请求的socket一样的属性,包括与之一样的异步选择事件(用 WSAAsyncSelect 或者WSAEventSelect 函数选择的事件)。然后,由新建的socket为accept()本次取出的连接请求服务,而原来的监听请求的socket又可以回到监听状态。
那么面向连接的socket的通信细节和无连接的有何不一样呢?这个需要研究面向连接的socket使用的数据读取和写入接口和其它接口。
accept(),接受客户端的连接请求,并生成一个 socket为这个客户服务。accept()的出口参数可以提供客户方的SockAddr,即地址。但是服务方返回一些数据没必要用这个地址,在面向连接的数据写入方法中,只需要一个socket就可以了, send()。而面向连接的数据读取方法recv()也只需要同一个socket就可以完成。所以这个通信过程细节如下:
服务端创建一个特殊的I/O --- socket,这个socket用来监听客户连接请求。所以,这个socket需要和服务端的本地地址帮扎。从前面知道,bind()地址就是从这个socket上读取数据的地址,不管是显式还是隐式。
然后服务器调用listen(socket, num),num是一个表示连接请求队列的最大值的整数。对于这个socket上并发的连接请求(请求连接绑扎地址),服务器不能马上响应的,就会被缓存字这个队列,等待服务器处理。但是队列满了以后,到来的请求就会不能被响应。listen()是非常关键的一步,只有调用了这一步,服务器才能监听客户端请求。
listen()以后服务器就调用accept(),提供一个出口参数可以获取请求方的地址。当指定的被accept的socket上的连接请求队列空,accept()会被阻塞。但是accept之前,服务器一直在listen请求。
如果这个socket上的连接请求缓存队列有连接请求,那么accept()就会脱离阻塞状态执行。accept()新建一个socket为从队列中取出的当前请求服务,而被accept的socket,或者也就是被listen()的那个socket()继续返回到listen()状态。面向连接的通信过程
如上图,服务器端创建socket1套接字,然后必须把该套接字和一个本地地址sockaddr1进行显式绑扎,这样就可以从socket1上读取数据的,或者说别人可以发送数据到这个地址。
随后,服务器在套接字socket1上调用listen(),进行请求的监听。listen()指定了请求缓冲队列的大小。listen之前的客户端连接请求connect()会失败。
调用listen()之后,服务器调用accept()从连接请求队列中取出一个连接请求,进行服务。如果队列空,accept被阻塞。accept()从队列中取出一个请求,并创建一个为这个取出的请求服务的套接字newSocket,并从出口参数返回该请求的客户端地址ClientAddr1。newSocket具有两个特点,第一个是具备与socket1一样的属性,这就是说newSocket也是绑扎在地址sockaddr1,这也就是从它读取数据的地址。另外一个是newSocket与ClientAddr1也具备了联系,这个地址是把数据写入newSocket的地方。所以,不需要取出出口参数的ClientAddr1这个客户端地址,仅仅通过新的套接字newSocket服务器端就和某个特定的客户端建立了全相关,就可以读取或者写入数据了。
再看客户端。客户端必须首先得到服务器的绑扎地址sockaddr1,客户端创建一个套接字socket2以后,就在该套接字上调用connect函数。connet()把一个本地地址ClientAddr1隐式绑扎到套接字socket2,作为数据接收地址。并且,connect把服务器地址sockaddr1和socket2联系起来。所以,通过socket2,客户端就可以进行读出和写入数据。面向连接三
如上图所示,全相关的建立过程。现在我们有个统一的观点,在一个socket上进行bind()一个本地地址(只能是本地地址才能被绑扎),就是本地程序在这个socket上的数据读取地址;但对通信的另一方来说就是数据的写入地址。服务器端为每一个不同的客户端产生一个newSocket进行服务,它们不同的地方就是这些newSocket具有不同的数据写入地址。但是具有一致的绑扎地址,尽管如此,不同的客户端发送的数据不会混淆,看来读取地址与socket句柄有关,所以不同的newSocket虽然具备同一个读取地址,但是会读到各自的数据。
客户端提前知道服务端地址,这是客户端的写地址。通过connect()请求,connect()隐式给客户端绑扎一个本地地址作为读取地址,并且显示绑扎服务端地址作为发送地址。服务器接受请求,并取得客户端地址,作为写地址。这样一来,双方的socket都具备了读写能力,所以建立了一个数据连接通道。刚开始学习使用博客,一不小心在博客园首页上发了好几篇, 第五篇就放到文章里啦:
socket编程与线程模型五