3、無連接socket與多線程
無連接socket很靈活,可以通過同一個socket 向很多個地址進行數據寫入,從同一個地址進行數據讀取。所以這種服務器的組 織形式也會很靈活。比如,利用多線程共享同一個服務器端的socket,進行數據 讀取和寫入。
但是需要注意,socket是特殊的I/O,既然屬於I/O,那麼 線程同步與互斥是非常重要的。因為它們讀寫socket的順序將不能被保證,或者 無法預料。理論上一個端口號對應於不同的緩沖區,也就是端口號是tcp/ip協議 棧上數據緩沖區的句柄。
五、有連接的socket1、概述
有連接的 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都具備了讀寫能力,所以建立了一個數 據連接通道。