
這一篇博客是繼上次的如何寫(xiě)一個(gè)簡(jiǎn)單的Web Server(一)的第二篇博客,拖了久,一直沒(méi)有寫(xiě),上個(gè)周把套接字通信的代碼重構(gòu)了一下,這周就打算把挖的坑填完。
Socket

這是維基百科上對(duì)socket的定義,socket中文叫套接字,說(shuō)實(shí)話我覺(jué)得這個(gè)翻譯不是很好,沒(méi)有接觸之前無(wú)法將套接字與網(wǎng)絡(luò)編程聯(lián)系起來(lái);
根據(jù)維基的定義,socket主要用于端對(duì)端的網(wǎng)絡(luò)通信,兩個(gè)端系統(tǒng)上的應(yīng)用程序通過(guò)兩個(gè)進(jìn)程對(duì)組成,從一個(gè)進(jìn)程向另一個(gè)進(jìn)程發(fā)發(fā)送報(bào)文必須通過(guò)套接字,我們可以把進(jìn)程想象成兩座房子,而它們的套接字相當(dāng)于它們的門(mén),當(dāng)一個(gè)主機(jī)向另一個(gè)主機(jī)發(fā)送報(bào)文時(shí),它需要將報(bào)文推出門(mén)(套接字),該發(fā)送進(jìn)程假設(shè)門(mén)與另一側(cè)之間有運(yùn)輸?shù)幕A(chǔ)設(shè)施,當(dāng)報(bào)文抵達(dá)目地主機(jī)時(shí),它通過(guò)接收進(jìn)程的門(mén)(套接字)傳遞,然后接收進(jìn)程對(duì)報(bào)文進(jìn)行處理。
多路復(fù)用和多路分解
在了解了套接字的定義后,自然而然的會(huì)想到在通信的過(guò)程中兩個(gè)通信進(jìn)程之間如何識(shí)別對(duì)方的問(wèn)題,這就涉及到運(yùn)輸層的多路分解和多路復(fù)用;在端系統(tǒng)上運(yùn)行的進(jìn)程有一個(gè)或者多個(gè)進(jìn)程,那么如何標(biāo)示一個(gè)特定的套接字顯然是一個(gè)問(wèn)題,在接收主機(jī)中從運(yùn)輸層來(lái)的數(shù)據(jù)沒(méi)有直接交付給進(jìn)程,而是通過(guò)一個(gè)中間的套接字來(lái)傳遞。由于在任一時(shí)刻接收主機(jī)上不止一個(gè)套接字,所以每個(gè)套接字都有唯一的標(biāo)識(shí)符,標(biāo)識(shí)符的格式取決于它是UDP套接字還是TCP套接字。
現(xiàn)在考慮接收主機(jī)如何將一個(gè)收到的運(yùn)輸層報(bào)文定向到合適的套接字,為了達(dá)到這一目的,在每個(gè)傳輸層報(bào)文中設(shè)置了幾個(gè)字段,在接收端運(yùn)輸層檢查這些字段來(lái)標(biāo)識(shí)出接收的套接字,然后將報(bào)文定向到對(duì)應(yīng)的套接字。將運(yùn)輸層報(bào)文交付到正確的套接字的工作稱為多路分解;從源主機(jī)的不同套接字中收集數(shù)據(jù)塊,并為每個(gè)數(shù)據(jù)塊封裝上首部信息(將在多路分解上使用)從而生成報(bào)文段,然后將報(bào)文段傳遞到網(wǎng)絡(luò)層的過(guò)程叫做多路復(fù)用。如下圖所示:

現(xiàn)在我們理解了運(yùn)輸層多路復(fù)與讀多路分解的作用,現(xiàn)在我們以UDP的多路復(fù)用與多路分解為例子來(lái)看看在主機(jī)中它們實(shí)際上是怎樣工作的,通過(guò)先前的內(nèi)容我們知道了運(yùn)輸層多路復(fù)用的要求:
1. socket要有唯一的標(biāo)識(shí)符
2. 每個(gè)報(bào)文端有特殊的字段來(lái)指示該報(bào)文段所要交付的套接字(如下圖所示這些特殊的字段是源端口號(hào)字段和目地端口字段)

如上圖所示,端口號(hào)由一個(gè)16比特的的數(shù)字,其大小在065535之間,01023范圍的端口號(hào)稱為周知端口號(hào),它們是保留給諸如HTTP(端口號(hào)80)和FTP(端口號(hào)21)之類的周知應(yīng)用層協(xié)議的,當(dāng)我們開(kāi)發(fā)一個(gè)新的應(yīng)用程序時(shí),必須為其分配一個(gè)端口號(hào)。
當(dāng)報(bào)文段到達(dá)主機(jī)時(shí),運(yùn)輸層檢查報(bào)文段中的目地端口號(hào),并將其定向到相應(yīng)的套接字,然后報(bào)文段的數(shù)據(jù)通過(guò)套接字進(jìn)入其連接的進(jìn)程。下面我們通過(guò)完成一個(gè)通信的程序來(lái)看看整個(gè)通信的過(guò)程。
這個(gè)程序分為client和server端,這里對(duì)server段的代碼進(jìn)行介紹,client的程序不做介紹,以下是整個(gè)過(guò)程的示意圖:

首先,我們考慮,如果你需要和你寫(xiě)的server端的程序進(jìn)行通信,你需要給server上的應(yīng)用進(jìn)程分配一個(gè)socket,并與其綁定,所以整個(gè)程序可以分為三步:
1. server創(chuàng)建套接字并與server綁定
2. server與client建立連接
3. server讀取報(bào)文后關(guān)閉連接
首先我們來(lái)看看創(chuàng)建socket的程序:
//
// @Brief: Create a socket for communicate with client.
void Server :: CreateSocket()
{
socket_file_description_ = socket(AF_INET, SOCK_STREAM, 0);
error_handler_.CheckSocketCreatedOrNot(socket_file_description_);
}
其中socket()函數(shù)的是在sys/socket.h中聲明的,其聲明為:
int socket(int family, int type, int protocol)
若成功返回非負(fù)描述符,若出錯(cuò)則為-1
其中fanily參數(shù)指明了協(xié)議族, type參數(shù)指明socket類型,protocal參數(shù)可以設(shè)為下圖中的某個(gè)常值,或者設(shè)為0,以選擇所給定family和type組合的系統(tǒng)默認(rèn)值,family、 type和protocol*的值如下圖所示:



socket函數(shù)在成功時(shí)返回一個(gè)非負(fù)整數(shù)值,它與文件描述符類似,我們稱它為套接字描述符,選擇有效的type和protocol的組合來(lái)創(chuàng)建套接字,有效的組合如下圖(部分組合):

創(chuàng)建好套接字后就需要將端口號(hào)和IP地址和套接字綁定,代碼如下:
//
// @Brief: Set the server address, port number
void Server :: SetServerAddress()
{
bzero((char*)&server_address_, sizeof(server_address_));
// convert the port number from string of digits to an interger.
port_number_ = atoi(argv_[1]);
// must be AF_INET which contain a code for the address family.
server_address_.sin_family = AF_INET;
// INADDR_ANY will get the IP address on which server runs.
server_address_.sin_addr.s_addr = INADDR_ANY;
// convert the port numberin host byte order to network order.
server_address_.sin_port = htons(port_number_);
}
//
// @Brief: Bind the socket with server.
void Server :: BindSocketWithServer()
{
int bind_flag = bind(socket_file_description_, (sockaddr*)
(&server_address_), sizeof(server_address_));
error_handler_.CheckBindOrNot(bind_flag);
listen(socket_file_description_, 5);
}
上述代碼中利用bind函數(shù)將一個(gè)本地協(xié)議地址賦予一個(gè)socket,對(duì)于IP協(xié)議,協(xié)議地址是32為的IPv4或者128位的IPv6地址與16位的TCP或者UDP端口號(hào)的組合,我們來(lái)看一看bind函數(shù):
*int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen)
綁定成功返回0,若出錯(cuò)返回-1
bind函數(shù)中第二個(gè)參數(shù)是指向特定協(xié)議的地址結(jié)構(gòu)結(jié)構(gòu)指針,第三個(gè)參數(shù)是該地址的長(zhǎng)度,對(duì)于TCP來(lái)說(shuō),調(diào)用bind函數(shù)可以指定一個(gè)端口號(hào),或者一個(gè)IP地址,也可以兩者都指定,也可以都不指定:
- 在服務(wù)器啟動(dòng)時(shí)會(huì)綁定他們眾所周知端口,對(duì)于TCP客戶或者服務(wù)器,如果bind的時(shí)候沒(méi)有指定綁定端口,當(dāng)調(diào)用connect或者listen的時(shí)候內(nèi)核就會(huì)為相應(yīng)的socket選擇一個(gè)臨時(shí)的端口。
- 進(jìn)程可以把一個(gè)特定的IP地址綁定到它的socket上,不過(guò)這個(gè)IP地址必須屬于其所在的主機(jī)的網(wǎng)絡(luò)接口之一,對(duì)于TCP客戶,這就為其指定了源IP地址。對(duì)于TCP服務(wù)器,這就限定該socket只接受目地IP為該服務(wù)器主機(jī)的客戶連接。但是對(duì)于TCP客戶機(jī)來(lái)一般不將IP地址綁定到其socket上,當(dāng)客戶機(jī)連接socket時(shí),內(nèi)核根據(jù)所用外出網(wǎng)絡(luò)的的接口來(lái)選擇源IP,而所用外出接口則取決于到達(dá)服務(wù)器所需的路徑,如果TCP服務(wù)器沒(méi)有把IP地址綁定到它的socket上,內(nèi)核就把客戶機(jī)發(fā)送的SYN的目地IP地址作為服務(wù)器的源IP地址。
調(diào)用bind的時(shí)候可以知道指定IP地址或者端口,可以兩者都指定,也可以兩者都不指定,如下圖所示(根據(jù)結(jié)果來(lái)設(shè)置sin_addr和sin_port或者sin6_addr和sin6_port)

對(duì)于IPv4來(lái)說(shuō),通配地址由常值INADDR_ANY來(lái)指定,其值一般為0,它告知內(nèi)核去選擇IP地址,如下面的語(yǔ)句:
struct sockaddr_in server_address;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
其中htonl是將主機(jī)序轉(zhuǎn)換成網(wǎng)絡(luò)序(如果需要查看更多關(guān)于這個(gè)函數(shù)的細(xì)節(jié),請(qǐng)自行去查看這個(gè)函數(shù)的man page)
但是INADDR_ANY的值無(wú)論在主機(jī)序還是在網(wǎng)絡(luò)序中,其值都是一樣的,因此使用htonl并非必需的。
將端口或IP綁定好之后,server就需要在這個(gè)端口上監(jiān)聽(tīng)連接了,監(jiān)聽(tīng)需要用到listen函數(shù),其聲明如下:
int listen(int sockfd, int backlog)
若成功返回0,若出錯(cuò)返回-1
listen函數(shù)僅由TCP服務(wù)器調(diào)用,它做兩件事:
- 當(dāng)socket被創(chuàng)建時(shí)候,他默認(rèn)被假設(shè)為一個(gè)主動(dòng)socket,即它被默認(rèn)是一個(gè)將要調(diào)用connect函數(shù)的客戶socket,而listen函數(shù)將一個(gè)未連接的socket轉(zhuǎn)換成一個(gè)被動(dòng)socket,指示內(nèi)核應(yīng)接受指向該socket的連接請(qǐng)求,根據(jù)TCP狀態(tài)轉(zhuǎn)換圖,服務(wù)器的連接是被動(dòng)打開(kāi)的,狀態(tài)由CLOSED轉(zhuǎn)移到LISTEN。
- 函數(shù)的第二個(gè)參數(shù)(backlog)規(guī)定了內(nèi)核應(yīng)該為相應(yīng)socket排隊(duì)的最大連接個(gè)數(shù)字;這里backlog參數(shù)我們做以下理解:
- 未完成連接隊(duì)列,即處于SYN_RCVD的等待完成TCP三次握手過(guò)程的連接。
- 已完成連接隊(duì)列,即已完成三次握手過(guò)程的連接,這些socket處于ESTABLISAHED狀態(tài)。
以上這兩個(gè)隊(duì)列之和不超過(guò)backlog
剩下的就是client和server建立連接之后讀寫(xiě)數(shù)據(jù)了,代碼如下:
//
// @Brief: Establish connect with client.
void Server :: EstablishConnect()
{
client_length_ = sizeof(client_address_);
while(1)
{
//establish the connection
new_socket_file_description_ = accept(socket_file_description_,
(sockaddr*) &client_address_, &client_length_);
error_handler_.CheckAcceptOrNot(new_socket_file_description_);
pid_ = fork(); //create a new process to handle this connection
if(pid_ < 0)
error_handler_.ErrorMessageDisplay("Error on fork");
if(pid_ == 0)
{
close(socket_file_description_);
DisplayMessageFromClient();
exit(0); // the process exits
}else{ // the parent closes the new socket file description
close(new_socket_file_description_);
}
}
}
參考文獻(xiàn):
unix網(wǎng)絡(luò)編程
Sockets Tutorial
Keep focus and have fun