天天看點

socket bufferedinputstream通信讀取不到伺服器傳回的響應_程序通信

程序間通信可以分為兩種類型,一種是通過作業系統本身提供的通信機制,另一種是使用socket進行網絡通信。

1. 作業系統内部通信

1.1. 多線程

一個程序,包括代碼、資料和配置設定給程序的資源。fork()函數通過系統調用建立一個與原來程序幾乎完全相同的程序,也就是兩個程序可以做完全相同的事,但如果初始參數或者傳入的變量不同,兩個程序也可以做不同的事。

一個程序調用fork()函數後,系統先給新的程序配置設定資源,例如存儲資料和代碼的空間。 然後把原來的程序的所有值都複制到新的新程序中,隻有少數值與原來的程序的值不同。相當于克隆了一個自己。

當pid==0時,表示的進入生成的子程序的代碼段中,否則,程序處于父程序的代碼段中。當fork傳回值小于0,則表示建立程序失敗。

1.2. 管道

建立無名管道

pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication. The array pipefd is used to return two file descriptors referring to the ends of the pipe. pipefd[0] refers to the read end of the pipe. pipefd[1] refers to the write end of the pipe. Data written to the write end of the pipe is buffered by the kernel until it is read from the read end of the pipe. For further details, see pipe(7).

建立有名管道

mkfifo

mkfifo ( name, mode)

Create named pipes (FIFOs) with the given NAMEs.

1.3. 信号量

一般用來同步程序順序。

signal(signum, sigaction), 将信号量與相應的動作綁定,一旦該信号量産生後,相應的動作将會被觸發執行。

kill( pid, signum), 向程序pid發送信号量signum。

采用信号量進行程序間同步時,出現不确定的情況(如書中的例子),目前未知是什麼原因。

1.4. 共享記憶體

#include <sys/types.h>

#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

int shmdt(const void *shmaddr);

ftok(pathname, id),利用id和pathname産生一個IPC key,即共享記憶體的辨別

shmget(key, size, mode),利用key産生唯一一塊大小為size的共享記憶體區,通路模式為mode,傳回記憶體辨別

shmat(shmid, NULL, 0),将程序私有記憶體映射到共享記憶體中,當第二個參數為NULL時,系統将自動找一個未用的記憶體區域。

shmdt(const void *shmaddr) 解除映射關系

1.5. 消息隊列

消息隊列實作包括穿件、添加、讀取和控制消息隊列這四種操作。

其中msgget(),建立和打開消息隊列;msgsnd()是将消息添加到消息隊列的末尾; msgrcv()是從消息隊列末尾擷取消息,也支援取走某條指定的消息;

1.6. 總結

學習IPC的要點就是熟知各個API接口的含義及其參數的意義,靈活運用。

程式設計流程可總結為:

1) 建立連接配接部件(如共享記憶體,消息隊列,信号量,管道)

2) 向連接配接部件中寫

3) 從連接配接部件中讀

4) 依次往複

其中,信号量機制主要用于同步程序執行,很少作為程序間通信的方式。

2. 網絡通信

要想了解socket首先得熟悉一下TCP/IP協定族, TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協定/網間協定,定義了主機如何連入網際網路及資料如何在它們之間傳輸的标準,從字面意思來看TCP/IP是TCP和IP協定的合稱,但實際上TCP/IP協定是指網際網路整個TCP/IP協定族。不同于ISO模型的七個分層,TCP/IP協定參考模型把所有的TCP/IP系列協定歸類到四個抽象層中

socket bufferedinputstream通信讀取不到伺服器傳回的響應_程式通信

Figure 1 Internet的通信協定層次劃分

在Internet層,解析IP位址,尋找通往目标IP的目的地的下一個路由位址。在網絡接口層,則是尋找響應的硬體(MAC)位址。資料流以及網絡拓撲結構如下圖所示。

socket bufferedinputstream通信讀取不到伺服器傳回的響應_程式通信

Figure 2 網絡拓撲以及資料流

我們知道兩個程序如果需要進行通訊最基本的一個前提能能夠唯一的标示一個程序,在本地程序通訊中我們可以使用PID來唯一标示一個程序,但PID隻在本地唯一,網絡中的兩個程序PID沖突幾率很大,這時候我們需要另辟它徑了,我們知道IP層的ip位址可以唯一标示主機,而TCP層協定和端口号可以唯一标示主機的一個程序,這樣我們可以利用ip位址+協定+端口号唯一标示網絡中的一個程序。

能夠唯一标示網絡中的程序後,它們就可以利用socket進行通信了,什麼是socket呢?我們經常把socket翻譯為套接字,socket是在應用層和傳輸層之間的一個抽象層,它把TCP/IP層複雜的操作抽象為幾個簡單的接口供應用層調用已實作程序在網絡中通信。

socket起源于UNIX,在Unix一切皆檔案哲學的思想下,socket是一種"打開—讀/寫—關閉"模式的實作,伺服器和用戶端各自維護一個"檔案",在建立連接配接打開後,可以向自己檔案寫入内容供對方讀取或者讀取對方内容,通訊結束時關閉檔案。

socket bufferedinputstream通信讀取不到伺服器傳回的響應_程式通信

Figure 3 Socket通信與網絡協定之間的層次關系

socket是"打開—讀/寫—關閉"模式的實作,以使用TCP協定通訊的socket為例,其互動流程大概是這樣子的

socket bufferedinputstream通信讀取不到伺服器傳回的響應_程式通信

Figure 4 socket 用戶端和服務端建立連接配接過程

l 伺服器根據位址類型(ipv4,ipv6)、socket類型、協定建立socket

l 伺服器為socket綁定ip位址和端口号

l 伺服器socket監聽端口号請求,随時準備接收用戶端發來的連接配接,這時候伺服器的socket并沒有被打開

l 用戶端建立socket

l 用戶端打開socket,根據伺服器ip位址和端口号試圖連接配接伺服器socket

l 伺服器socket接收到用戶端socket請求,被動打開,開始接收用戶端請求,直到用戶端傳回連接配接資訊。這時候socket進入阻塞狀态,所謂阻塞即accept()方法一直到用戶端傳回連接配接資訊後才傳回,開始接收下一個用戶端諒解請求

l 用戶端連接配接成功,向伺服器發送連接配接狀态資訊

l 伺服器accept方法傳回,連接配接成功

l 用戶端向socket寫入資訊

l 伺服器讀取資訊

l 用戶端關閉

l 伺服器端關閉

2.1. TCP

TCP/IP協定中的三次握手,TCP協定通過三次握手建立一個可靠的連接配接。

socket bufferedinputstream通信讀取不到伺服器傳回的響應_程式通信

Figure 5 TCP通過三次握手建立安全連接配接

第一次握手:用戶端嘗試連接配接伺服器,向伺服器發送syn包(同步序列編号Synchronize Sequence Numbers),syn=j,用戶端進入SYN_SEND狀态等待伺服器确認

第二次握手:伺服器接收用戶端syn包并确認(ack=j+1),同時向用戶端發送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀态

第三次握手:第三次握手:用戶端收到伺服器的SYN+ACK包,向伺服器發送确認包ACK(ack=k+1),此包發送完畢,用戶端和伺服器進入ESTABLISHED狀态,完成三次握手

定睛一看,伺服器socket與用戶端socket建立連接配接的部分其實就是大名鼎鼎的三次握手

socket bufferedinputstream通信讀取不到伺服器傳回的響應_程式通信

Figure 6 TCP在SOCKET中的三次握手過程

2.2. UDP

UDP和TCP程式設計步驟有些不同,如下:

TCP程式設計的伺服器端一般步驟是:

1、建立一個socket,用函數socket();

2、設定socket屬性,用函數setsockopt(); * 可選

3、綁定IP位址、端口等資訊到socket上,用函數bind();

4、開啟監聽,用函數listen();

5、接收用戶端上來的連接配接,用函數accept();

6、收發資料,用函數send()和recv(),或者read()和write();

7、關閉網絡連接配接;

8、關閉監聽;

TCP程式設計的用戶端一般步驟是:

1、建立一個socket,用函數socket();

2、設定socket屬性,用函數setsockopt();* 可選

3、綁定IP位址、端口等資訊到socket上,用函數bind();* 可選

4、設定要連接配接的對方的IP位址和端口等屬性;

5、連接配接伺服器,用函數connect();

6、收發資料,用函數send()和recv(),或者read()和write();

7、關閉網絡連接配接;

與之對應的UDP程式設計步驟要簡單許多,分别如下:

UDP程式設計的伺服器端一般步驟是:

1、建立一個socket,用函數socket();

2、設定socket屬性,用函數setsockopt();* 可選

3、綁定IP位址、端口等資訊到socket上,用函數bind();

4、循環接收資料,用函數recvfrom();

5、關閉網絡連接配接;

UDP程式設計的用戶端一般步驟是:

1、建立一個socket,用函數socket();

2、設定socket屬性,用函數setsockopt();* 可選

3、綁定IP位址、端口等資訊到socket上,用函數bind();* 可選

4、設定對方的IP位址和端口等屬性;

5、發送資料,用函數sendto();

6、關閉網絡連接配接;

其中udp通信過程中不需要建立連接配接,也不要進行三次握手的過程。UDP用戶端直接向服務體端發送資料,服務端循環讀取相應位址的資料資訊。

recvfrom和sendto的參數中包含了通信的位址資訊,在傳輸資料的同時,能夠找尋通信目标位址。

2.3. Select

select函數的作用:

select()在SOCKET程式設計中還是比較重要的,可是對于初學SOCKET的人來說都不太愛用select()寫程式,他們隻是習慣寫諸如 conncet()、accept()、recv()或recvfrom這樣的阻塞程式(所謂阻塞方式block,顧名思義,就是程序或是線程執行到這些函數時必須等待某個事件發生,如果事件沒有發生,程序或線程就被阻塞,函數不能立即傳回)。可是使用select()就可以完成非阻塞(所謂非阻塞方式non-block,就是程序或線程執行此函數時不必非要等待事件的發生,一旦執行肯定傳回,以傳回值的不同來反映函數的執行情況。如果事件發生則與阻塞方式相同,若事件沒有發生則傳回一個代碼來告知事件未發生,而程序或線程繼續執行,是以效率高)方式工作的程式,它能夠監視我們需要監視的檔案描述符的變化情況——讀寫或是異常。

select函數格式:

select()函數的格式(所說的是Unix系統下的Berkeley Socket程式設計,和Windows下的有差別,一會兒說明):

Unix系統下解釋:

int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);

先說明兩個結構體

第一:struct fd_set可以了解為一個集合,這個集合中存放的是檔案描述符(file descriptor),即檔案句柄,這可以是我們所說的普通意義的檔案,當然Unix下任何裝置、管道、FIFO等都是檔案形式,全部包括在内,是以,毫無疑問,一個socket就是一個檔案,socket句柄就是一個檔案描述符。fd_set集合可以通過一些宏由人為來操作,比如清空集合:FD_ZERO(fd_set*),将一個給定的檔案描述符加入集合之中FD_SET(int, fd_set*),将一個給定的檔案描述符從集合中删除FD_CLR(int, fd_set*),檢查集合中指定的檔案描述符是否可以讀寫FD_ISSET(int, fd_set*)。一會兒舉例說明。

第二:struct timeval是一個大家常用的結構,用來代表時間值,有兩個成員,一個是秒數,另一個毫秒數。

具體解釋select的參數:

int maxfdp是一個整數值,是指集合中所有檔案描述符的範圍,即所有檔案描述符的最大值加1,不能錯!在Windows中這個參數值無所謂,可以設定不正确。

fd_set* readfds是指向fd_set結構的指針,這個集合中應該包括檔案描述符,我們是要監視這些檔案描述符的讀變化的,即我們關心是否可以從這些檔案中讀取資料了,如果這個集合中有一個檔案可讀,select就會傳回一個大于0的值,表示有檔案可讀,如果沒有可讀的檔案,則根據timeout參數再判斷是否逾時,若超出timeout的時間,select傳回0,若發生錯誤傳回負值。可以傳入NULL值,表示不關心任何檔案的讀變化。

fd_set* writefds是指向fd_set結構的指針,這個集合中應該包括檔案描述符,我們是要監視這些檔案描述符的寫變化的,即我們關心是否可以向這些檔案中寫入資料了,如果這個集合中有一個檔案可寫,select就會傳回一個大于0的值,表示有檔案可寫,如果沒有可寫的檔案,則根據timeout再判斷是否逾時,若超出timeout的時間,select傳回0,若發生錯誤傳回負值。可以傳入NULL值,表示不關心任何檔案的寫變化。

fe_set* errorfds同上面兩個參數的意圖,用來監視檔案錯誤異常。

struct timeval* timeout是select的逾時時間,這個參數至關重要,它可以使select處于三種狀态。

第一:若将NULL以形參傳入,即不傳入時間結構,就是将select置于阻塞狀态,一定等到監視檔案描述符集合中某個檔案描述符發生變化為止;

第二:若将時間值設為0秒0毫秒,就變成一個純粹的非阻塞函數,不管檔案描述符是否有變化,都立刻傳回繼續執行,檔案無變化傳回0,有變化傳回一個正值;

第三:timeout的值大于0,這就是等待的逾時時間,即select在timeout時間内阻塞,逾時時間之内有事件到來就傳回了,否則在逾時後不管怎樣一定傳回,傳回值同上述。

select函數傳回值:

負值:select錯誤

正值:某些檔案可讀寫或出錯

0:等待逾時,沒有可讀寫或錯誤的檔案