国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
linux 編程
Linux系統(tǒng)編程---socket編程
1. 預(yù)備知識(shí)
1.1. 網(wǎng)絡(luò)字節(jié)序
我們已經(jīng)知道,內(nèi)存中的多字節(jié)數(shù)據(jù)相對(duì)于內(nèi)存地址有大端和小端之分,磁盤文件中的多字節(jié)數(shù)據(jù)相對(duì)于文件中的偏移地址也有大端小端之分。網(wǎng)絡(luò)數(shù)據(jù)流同樣有大端小端之分,那么如何定義網(wǎng)絡(luò)數(shù)據(jù)流的地址呢?發(fā)送主機(jī)通常將發(fā)送緩沖區(qū)中的數(shù)據(jù)按內(nèi)存地址從低到高的順序發(fā)出,接收主機(jī)把從網(wǎng)絡(luò)上接到的字節(jié)依次保存在接收緩沖區(qū)中,也是按內(nèi)存地址從低到高的順序保存,因此,網(wǎng)絡(luò)數(shù)據(jù)流的地址應(yīng)這樣規(guī)定:先發(fā)出的數(shù)據(jù)是低地址,后發(fā)出的數(shù)據(jù)是高地址。
TCP/IP協(xié)議規(guī)定,網(wǎng)絡(luò)數(shù)據(jù)流應(yīng)采用大端字節(jié)序,即低地址高字節(jié)。例如上一節(jié)的UDP段格式,地址0-1是16位的源端口號(hào),如果這個(gè)端口號(hào)是1000(0x3e8),則地址0是0x03,地址1是0xe8,也就是先發(fā)0x03,再發(fā)0xe8,這16位在發(fā)送主機(jī)的緩沖區(qū)中也應(yīng)該是低地址存0x03,高地址存0xe8。但是,如果發(fā)送主機(jī)是小端字節(jié)序的,這16位被解釋成0xe803,而不是1000。因此,發(fā)送主機(jī)把1000填到發(fā)送緩沖區(qū)之前需要做字節(jié)序的轉(zhuǎn)換。同樣地,接收主機(jī)如果是小端字節(jié)序的,接到16位的源端口號(hào)也要做字節(jié)序的轉(zhuǎn)換。如果主機(jī)是大端字節(jié)序的,發(fā)送和接收都不需要做轉(zhuǎn)換。同理,32位的IP地址也要考慮網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序的問題。
為使網(wǎng)絡(luò)程序具有可移植性,使同樣的C代碼在大端和小端計(jì)算機(jī)上編譯后都能正常運(yùn)行,可以調(diào)用以下庫(kù)函數(shù)做網(wǎng)絡(luò)字節(jié)序和主機(jī)字節(jié)序的轉(zhuǎn)換。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
這些函數(shù)名很好記,h表示host,n表示network,l表示32位長(zhǎng)整數(shù),s表示16位短整數(shù)。例如htonl表示將32位的長(zhǎng)整數(shù)從主機(jī)字節(jié)序轉(zhuǎn)換為網(wǎng)絡(luò)字節(jié)序,例如將IP地址轉(zhuǎn)換后準(zhǔn)備發(fā)送。如果主機(jī)是小端字節(jié)序,這些函數(shù)將參數(shù)做相應(yīng)的大小端轉(zhuǎn)換然后返回,如果主機(jī)是大端字節(jié)序,這些函數(shù)不做轉(zhuǎn)換,將參數(shù)原封不動(dòng)地返回。
1.2. socket地址的數(shù)據(jù)類型及相關(guān)函數(shù)
socket API是一層抽象的網(wǎng)絡(luò)編程接口,適用于各種底層網(wǎng)絡(luò)協(xié)議,如IPv4、IPv6,以及后面要講的UNIX Domain Socket。然而,各種網(wǎng)絡(luò)協(xié)議的地址格式并不相同,如下圖所示:
圖 37.1. sockaddr數(shù)據(jù)結(jié)構(gòu)
IPv4和IPv6的地址格式定義在netinet/in.h中,IPv4地址用sockaddr_in結(jié)構(gòu)體表示,包括16位端口號(hào)和32位IP地址,IPv6地址用sockaddr_in6結(jié)構(gòu)體表示,包括16位端口號(hào)、128位IP地址和一些控制字段。UNIX Domain Socket的地址格式定義在sys/un.h中,用sockaddr_un結(jié)構(gòu)體表示。各種socket地址結(jié)構(gòu)體的開頭都是相同的,前16位表示整個(gè)結(jié)構(gòu)體的長(zhǎng)度(并不是所有UNIX的實(shí)現(xiàn)都有長(zhǎng)度字段,如Linux就沒有),后16位表示地址類型。IPv4、IPv6和UNIX Domain Socket的地址類型分別定義為常數(shù)AF_INET、AF_INET6、AF_UNIX。這樣,只要取得某種sockaddr結(jié)構(gòu)體的首地址,不需要知道具體是哪種類型的sockaddr結(jié)構(gòu)體,就可以根據(jù)地址類型字段確定結(jié)構(gòu)體中的內(nèi)容。因此,socket API可以接受各種類型的sockaddr結(jié)構(gòu)體指針做參數(shù),例如bind、accept、connect等函數(shù),這些函數(shù)的參數(shù)應(yīng)該設(shè)計(jì)成void *類型以便接受各種類型的指針,但是sock API的實(shí)現(xiàn)早于ANSI C標(biāo)準(zhǔn)化,那時(shí)還沒有void *類型,因此這些函數(shù)的參數(shù)都用struct sockaddr *類型表示,在傳遞參數(shù)之前要強(qiáng)制類型轉(zhuǎn)換一下,例如:
struct sockaddr_in servaddr;
/* initialize servaddr */
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
本節(jié)只介紹基于IPv4的socket網(wǎng)絡(luò)編程,sockaddr_in中的成員struct in_addr sin_addr表示32位的IP地址。但是我們通常用點(diǎn)分十進(jìn)制的字符串表示IP地址,以下函數(shù)可以在字符串表示和in_addr表示之間轉(zhuǎn)換。
字符串轉(zhuǎn)in_addr的函數(shù):
#include <arpa/inet.h>
int inet_aton(const char *strptr, struct in_addr *addrptr);
in_addr_t inet_addr(const char *strptr);
int inet_pton(int family, const char *strptr, void *addrptr);
in_addr轉(zhuǎn)字符串的函數(shù):
char *inet_ntoa(struct in_addr inaddr);
const char *inet_ntop(int family, const void *addrptr, char *strptr,
size_t len);
其中inet_pton和inet_ntop不僅可以轉(zhuǎn)換IPv4的in_addr,還可以轉(zhuǎn)換IPv6的in6_addr,因此函數(shù)接口是void *addrptr。
2. 基于TCP協(xié)議的網(wǎng)絡(luò)程序
下圖是基于TCP協(xié)議的客戶端/服務(wù)器程序的一般流程:
圖 37.2. TCP協(xié)議通訊流程
服務(wù)器調(diào)用socket()、bind()、listen()完成初始化后,調(diào)用accept()阻塞等待,處于監(jiān)聽端口的狀態(tài),客戶端調(diào)用socket()初始化后,調(diào)用connect()發(fā)出SYN段并阻塞等待服務(wù)器應(yīng)答,服務(wù)器應(yīng)答一個(gè)SYN-ACK段,客戶端收到后從connect()返回,同時(shí)應(yīng)答一個(gè)ACK段,服務(wù)器收到后從accept()返回。
數(shù)據(jù)傳輸?shù)倪^程:
建立連接后,TCP協(xié)議提供全雙工的通信服務(wù),但是一般的客戶端/服務(wù)器程序的流程是由客戶端主動(dòng)發(fā)起請(qǐng)求,服務(wù)器被動(dòng)處理請(qǐng)求,一問一答的方式。因此,服務(wù)器從accept()返回后立刻調(diào)用read(),讀socket就像讀管道一樣,如果沒有數(shù)據(jù)到達(dá)就阻塞等待,這時(shí)客戶端調(diào)用write()發(fā)送請(qǐng)求給服務(wù)器,服務(wù)器收到后從read()返回,對(duì)客戶端的請(qǐng)求進(jìn)行處理,在此期間客戶端調(diào)用read()阻塞等待服務(wù)器的應(yīng)答,服務(wù)器調(diào)用write()將處理結(jié)果發(fā)回給客戶端,再次調(diào)用read()阻塞等待下一條請(qǐng)求,客戶端收到后從read()返回,發(fā)送下一條請(qǐng)求,如此循環(huán)下去。
如果客戶端沒有更多的請(qǐng)求了,就調(diào)用close()關(guān)閉連接,就像寫端關(guān)閉的管道一樣,服務(wù)器的read()返回0,這樣服務(wù)器就知道客戶端關(guān)閉了連接,也調(diào)用close()關(guān)閉連接。注意,任何一方調(diào)用close()后,連接的兩個(gè)傳輸方向都關(guān)閉,不能再發(fā)送數(shù)據(jù)了。如果一方調(diào)用shutdown()則連接處于半關(guān)閉狀態(tài),仍可接收對(duì)方發(fā)來的數(shù)據(jù)。
在學(xué)習(xí)socket API時(shí)要注意應(yīng)用程序和TCP協(xié)議層是如何交互的: *應(yīng)用程序調(diào)用某個(gè)socket函數(shù)時(shí)TCP協(xié)議層完成什么動(dòng)作,比如調(diào)用connect()會(huì)發(fā)出SYN段 *應(yīng)用程序如何知道TCP協(xié)議層的狀態(tài)變化,比如從某個(gè)阻塞的socket函數(shù)返回就表明TCP協(xié)議收到了某些段,再比如read()返回0就表明收到了FIN段
2.1. 最簡(jiǎn)單的TCP網(wǎng)絡(luò)程序
下面通過最簡(jiǎn)單的客戶端/服務(wù)器程序的實(shí)例來學(xué)習(xí)socket API。
server.c的作用是從客戶端讀字符,然后將每個(gè)字符轉(zhuǎn)換為大寫并回送給客戶端。
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
write(connfd, buf, n);
close(connfd);
}
}
下面介紹程序中用到的socket API,這些函數(shù)都在sys/socket.h中。
int socket(int family, int type, int protocol);
socket()打開一個(gè)網(wǎng)絡(luò)通訊端口,如果成功的話,就像open()一樣返回一個(gè)文件描述符,應(yīng)用程序可以像讀寫文件一樣用read/write在網(wǎng)絡(luò)上收發(fā)數(shù)據(jù),如果socket()調(diào)用出錯(cuò)則返回-1。對(duì)于IPv4,family參數(shù)指定為AF_INET。對(duì)于TCP協(xié)議,type參數(shù)指定為SOCK_STREAM,表示面向流的傳輸協(xié)議。如果是UDP協(xié)議,則type參數(shù)指定為SOCK_DGRAM,表示面向數(shù)據(jù)報(bào)的傳輸協(xié)議。protocol參數(shù)的介紹從略,指定為0即可。
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
服務(wù)器程序所監(jiān)聽的網(wǎng)絡(luò)地址和端口號(hào)通常是固定不變的,客戶端程序得知服務(wù)器程序的地址和端口號(hào)后就可以向服務(wù)器發(fā)起連接,因此服務(wù)器需要調(diào)用bind綁定一個(gè)固定的網(wǎng)絡(luò)地址和端口號(hào)。bind()成功返回0,失敗返回-1。
bind()的作用是將參數(shù)sockfd和myaddr綁定在一起,使sockfd這個(gè)用于網(wǎng)絡(luò)通訊的文件描述符監(jiān)聽myaddr所描述的地址和端口號(hào)。前面講過,struct sockaddr *是一個(gè)通用指針類型,myaddr參數(shù)實(shí)際上可以接受多種協(xié)議的sockaddr結(jié)構(gòu)體,而它們的長(zhǎng)度各不相同,所以需要第三個(gè)參數(shù)addrlen指定結(jié)構(gòu)體的長(zhǎng)度。我們的程序中對(duì)myaddr參數(shù)是這樣初始化的:
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
首先將整個(gè)結(jié)構(gòu)體清零,然后設(shè)置地址類型為AF_INET,網(wǎng)絡(luò)地址為INADDR_ANY,這個(gè)宏表示本地的任意IP地址,因?yàn)榉?wù)器可能有多個(gè)網(wǎng)卡,每個(gè)網(wǎng)卡也可能綁定多個(gè)IP地址,這樣設(shè)置可以在所有的IP地址上監(jiān)聽,直到與某個(gè)客戶端建立了連接時(shí)才確定下來到底用哪個(gè)IP地址,端口號(hào)為SERV_PORT,我們定義為8000。
int listen(int sockfd, int backlog);
典型的服務(wù)器程序可以同時(shí)服務(wù)于多個(gè)客戶端,當(dāng)有客戶端發(fā)起連接時(shí),服務(wù)器調(diào)用的accept()返回并接受這個(gè)連接,如果有大量的客戶端發(fā)起連接而服務(wù)器來不及處理,尚未accept的客戶端就處于連接等待狀態(tài),listen()聲明sockfd處于監(jiān)聽狀態(tài),并且最多允許有backlog個(gè)客戶端處于連接待狀態(tài),如果接收到更多的連接請(qǐng)求就忽略。listen()成功返回0,失敗返回-1。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
三方握手完成后,服務(wù)器調(diào)用accept()接受連接,如果服務(wù)器調(diào)用accept()時(shí)還沒有客戶端的連接請(qǐng)求,就阻塞等待直到有客戶端連接上來。cliaddr是一個(gè)傳出參數(shù),accept()返回時(shí)傳出客戶端的地址和端口號(hào)。addrlen參數(shù)是一個(gè)傳入傳出參數(shù)(value-result argument),傳入的是調(diào)用者提供的緩沖區(qū)cliaddr的長(zhǎng)度以避免緩沖區(qū)溢出問題,傳出的是客戶端地址結(jié)構(gòu)體的實(shí)際長(zhǎng)度(有可能沒有占滿調(diào)用者提供的緩沖區(qū))。如果給cliaddr參數(shù)傳NULL,表示不關(guān)心客戶端的地址。
我們的服務(wù)器程序結(jié)構(gòu)是這樣的:
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
...
close(connfd);
}
整個(gè)是一個(gè)while死循環(huán),每次循環(huán)處理一個(gè)客戶端連接。由于cliaddr_len是傳入傳出參數(shù),每次調(diào)用accept()之前應(yīng)該重新賦初值。accept()的參數(shù)listenfd是先前的監(jiān)聽文件描述符,而accept()的返回值是另外一個(gè)文件描述符connfd,之后與客戶端之間就通過這個(gè)connfd通訊,最后關(guān)閉connfd斷開連接,而不關(guān)閉listenfd,再次回到循環(huán)開頭listenfd仍然用作accept的參數(shù)。accept()成功返回一個(gè)文件描述符,出錯(cuò)返回-1。
client.c的作用是從命令行參數(shù)中獲得一個(gè)字符串發(fā)給服務(wù)器,然后接收服務(wù)器返回的字符串并打印。
/* client.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
char *str;
if (argc != 2) {
fputs("usage: ./client message\n", stderr);
exit(1);
}
str = argv[1];
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, str, strlen(str));
n = read(sockfd, buf, MAXLINE);
printf("Response from server:\n");
write(STDOUT_FILENO, buf, n);
close(sockfd);
return 0;
}
由于客戶端不需要固定的端口號(hào),因此不必調(diào)用bind(),客戶端的端口號(hào)由內(nèi)核自動(dòng)分配。注意,客戶端不是不允許調(diào)用bind(),只是沒有必要調(diào)用bind()固定一個(gè)端口號(hào),服務(wù)器也不是必須調(diào)用bind(),但如果服務(wù)器不調(diào)用bind(),內(nèi)核會(huì)自動(dòng)給服務(wù)器分配監(jiān)聽端口,每次啟動(dòng)服務(wù)器時(shí)端口號(hào)都不一樣,客戶端要連接服務(wù)器就會(huì)遇到麻煩。
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
客戶端需要調(diào)用connect()連接服務(wù)器,connect和bind的參數(shù)形式一致,區(qū)別在于bind的參數(shù)是自己的地址,而connect的參數(shù)是對(duì)方的地址。connect()成功返回0,出錯(cuò)返回-1。
先編譯運(yùn)行服務(wù)器:
$ ./server
Accepting connections ...
然后在另一個(gè)終端里用netstat命令查看:
$ netstat -apn|grep 8000
tcp        0      0 0.0.0.0:8000            0.0.0.0:*
LISTEN     8148/server
可以看到server程序監(jiān)聽8000端口,IP地址還沒確定下來?,F(xiàn)在編譯運(yùn)行客戶端:
$ ./client abcd
Response from server:
ABCD
回到server所在的終端,看看server的輸出:
$ ./server
Accepting connections ...
received from 127.0.0.1 at PORT 59757
可見客戶端的端口號(hào)是自動(dòng)分配的?,F(xiàn)在把客戶端所連接的服務(wù)器IP改為其它主機(jī)的IP,試試兩臺(tái)主機(jī)的通訊。
再做一個(gè)小實(shí)驗(yàn),在客戶端的connect()代碼之后插一個(gè)while(1);死循環(huán),使客戶端和服務(wù)器都處于連接中的狀態(tài),用netstat命令查看:
$ ./server &
[1] 8343
$ Accepting connections ...
./client abcd &
[2] 8344
$ netstat -apn|grep 8000
tcp        0      0 0.0.0.0:8000            0.0.0.0:*               LISTEN     8343/server
tcp        0      0 127.0.0.1:44406         127.0.0.1:8000          ESTABLISHED8344/client
tcp        0      0 127.0.0.1:8000          127.0.0.1:44406         ESTABLISHED8343/server
應(yīng)用程序中的一個(gè)socket文件描述符對(duì)應(yīng)一個(gè)socket pair,也就是源地址:源端口號(hào)和目的地址:目的端口號(hào),也對(duì)應(yīng)一個(gè)TCP連接。
表 37.1. client和server的socket狀態(tài)
socket文件描述符
源地址:源端口號(hào)
目的地址:目的端口號(hào)
狀態(tài)
server.c中的listenfd
0.0.0.0:8000
0.0.0.0:*
LISTEN
server.c中的connfd
127.0.0.1:8000
127.0.0.1:44406
ESTABLISHED
client.c中的sockfd
127.0.0.1:44406
127.0.0.1:8000
ESTABLISHED
2.2. 錯(cuò)誤處理與讀寫控制
上面的例子不僅功能簡(jiǎn)單,而且簡(jiǎn)單到幾乎沒有什么錯(cuò)誤處理,我們知道,系統(tǒng)調(diào)用不能保證每次都成功,必須進(jìn)行出錯(cuò)處理,這樣一方面可以保證程序邏輯正常,另一方面可以迅速得到故障信息。
為使錯(cuò)誤處理的代碼不影響主程序的可讀性,我們把與socket相關(guān)的一些系統(tǒng)函數(shù)加上錯(cuò)誤處理代碼包裝成新的函數(shù),做成一個(gè)模塊wrap.c:
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
void perr_exit(const char *s)
{
perror(s);
exit(1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ( (n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
perr_exit("accept error");
}
return n;
}
void Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
if (bind(fd, sa, salen) < 0)
perr_exit("bind error");
}
void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
if (connect(fd, sa, salen) < 0)
perr_exit("connect error");
}
void Listen(int fd, int backlog)
{
if (listen(fd, backlog) < 0)
perr_exit("listen error");
}
int Socket(int family, int type, int protocol)
{
int n;
if ( (n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
void Close(int fd)
{
if (close(fd) == -1)
perr_exit("close error");
}
慢系統(tǒng)調(diào)用accept、read和write被信號(hào)中斷時(shí)應(yīng)該重試。connect雖然也會(huì)阻塞,但是被信號(hào)中斷時(shí)不能立刻重試。對(duì)于accept,如果errno是ECONNABORTED,也應(yīng)該重試。詳細(xì)解釋見參考資料。
TCP協(xié)議是面向流的,read和write調(diào)用的返回值往往小于參數(shù)指定的字節(jié)數(shù)。對(duì)于read調(diào)用,如果接收緩沖區(qū)中有20字節(jié),請(qǐng)求讀100個(gè)字節(jié),就會(huì)返回20。對(duì)于write調(diào)用,如果請(qǐng)求寫100個(gè)字節(jié),而發(fā)送緩沖區(qū)中只有20個(gè)字節(jié)的空閑位置,那么write會(huì)阻塞,直到把100個(gè)字節(jié)全部交給發(fā)送緩沖區(qū)才返回,但如果socket文件描述符有O_NONBLOCK標(biāo)志,則write不阻塞,直接返回20。為避免這些情況干擾主程序的邏輯,確保讀寫我們所請(qǐng)求的字節(jié)數(shù),我們實(shí)現(xiàn)了兩個(gè)包裝函數(shù)readn和writen,也放在wrap.c中:
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t  nleft;
ssize_t nread;
char   *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
如果應(yīng)用層協(xié)議的各字段長(zhǎng)度固定,用readn來讀是非常方便的。例如設(shè)計(jì)一種客戶端上傳文件的協(xié)議,規(guī)定前12字節(jié)表示文件名,超過12字節(jié)的文件名截?cái)啵蛔?2字節(jié)的文件名用'\0'補(bǔ)齊,從第13字節(jié)開始是文件內(nèi)容,上傳完所有文件內(nèi)容后關(guān)閉連接,服務(wù)器可以先調(diào)用readn讀12個(gè)字節(jié),根據(jù)文件名創(chuàng)建文件,然后在一個(gè)循環(huán)中調(diào)用read讀文件內(nèi)容并存盤,循環(huán)結(jié)束的條件是read返回0。
字段長(zhǎng)度固定的協(xié)議往往不夠靈活,難以適應(yīng)新的變化。比如,以前DOS的文件名是8字節(jié)主文件名加“.”加3字節(jié)擴(kuò)展名,不超過12字節(jié),但是現(xiàn)代操作系統(tǒng)的文件名可以長(zhǎng)得多,12字節(jié)就不夠用了。那么制定一個(gè)新版本的協(xié)議規(guī)定文件名字段為256字節(jié)怎么樣?這樣又造成很大的浪費(fèi),因?yàn)榇蠖鄶?shù)文件名都很短,需要用大量的'\0'補(bǔ)齊256字節(jié),而且新版本的協(xié)議和老版本的程序無法兼容,如果已經(jīng)有很多人在用老版本的程序了,會(huì)造成遵循新協(xié)議的程序與老版本程序的互操作性(Interoperability)問題。如果新版本的協(xié)議要添加新的字段,比如規(guī)定前12字節(jié)是文件名,從13到16字節(jié)是文件類型說明,從第17字節(jié)開始才是文件內(nèi)容,同樣會(huì)造成和老版本的程序無法兼容的問題。
現(xiàn)在重新看看上一節(jié)的TFTP協(xié)議是如何避免上述問題的:TFTP協(xié)議的各字段是可變長(zhǎng)的,以'\0'為分隔符,文件名可以任意長(zhǎng),再看blksize等幾個(gè)選項(xiàng)字段,TFTP協(xié)議并沒有規(guī)定從第m字節(jié)到第n字節(jié)是blksize的值,而是把選項(xiàng)的描述信息“blksize”與它的值“512”一起做成一個(gè)可變長(zhǎng)的字段,這樣,以后添加新的選項(xiàng)仍然可以和老版本的程序兼容(老版本的程序只要忽略不認(rèn)識(shí)的選項(xiàng)就行了)。
因此,常見的應(yīng)用層協(xié)議都是帶有可變長(zhǎng)字段的,字段之間的分隔符用換行的比用'\0'的更常見,例如本節(jié)后面要介紹的HTTP協(xié)議??勺冮L(zhǎng)字段的協(xié)議用readn來讀就很不方便了,為此我們實(shí)現(xiàn)一個(gè)類似于fgets的readline函數(shù),也放在wrap.c中:
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char    c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c  == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr  = 0;
return n;
}
習(xí)題
1、請(qǐng)讀者自己寫出wrap.c的頭文件wrap.h,后面的網(wǎng)絡(luò)程序代碼都要用到這個(gè)頭文件。
2、修改server.c和client.c,添加錯(cuò)誤處理。
2.3. 把client改為交互式輸入
目前實(shí)現(xiàn)的client每次運(yùn)行只能從命令行讀取一個(gè)字符串發(fā)給服務(wù)器,再?gòu)姆?wù)器收回來,現(xiàn)在我們把它改成交互式的,不斷從終端接受用戶輸入并和server交互。
/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
Connect(sockfd, (struct sockaddr *)&servaddr,
sizeof(servaddr));
while (fgets(buf, MAXLINE, stdin) != NULL) {
Write(sockfd, buf, strlen(buf));
n = Read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
編譯并運(yùn)行server和client,看看是否達(dá)到了你預(yù)想的結(jié)果。
$ ./client
haha1
HAHA1
haha2
the other side has been closed.
haha3
$
這時(shí)server仍在運(yùn)行,但是client的運(yùn)行結(jié)果并不正確。原因是什么呢?仔細(xì)查看server.c可以發(fā)現(xiàn),server對(duì)每個(gè)請(qǐng)求只處理一次,應(yīng)答后就關(guān)閉連接,client不能繼續(xù)使用這個(gè)連接發(fā)送數(shù)據(jù)。但是client下次循環(huán)時(shí)又調(diào)用write發(fā)數(shù)據(jù)給server,write調(diào)用只負(fù)責(zé)把數(shù)據(jù)交給TCP發(fā)送緩沖區(qū)就可以成功返回了,所以不會(huì)出錯(cuò),而server收到數(shù)據(jù)后應(yīng)答一個(gè)RST段,client收到RST段后無法立刻通知應(yīng)用層,只把這個(gè)狀態(tài)保存在TCP協(xié)議層。client下次循環(huán)又調(diào)用write發(fā)數(shù)據(jù)給server,由于TCP協(xié)議層已經(jīng)處于RST狀態(tài)了,因此不會(huì)將數(shù)據(jù)發(fā)出,而是發(fā)一個(gè)SIGPIPE信號(hào)給應(yīng)用層,SIGPIPE信號(hào)的缺省處理動(dòng)作是終止程序,所以看到上面的現(xiàn)象。
為了避免client異常退出,上面的代碼應(yīng)該在判斷對(duì)方關(guān)閉了連接后break出循環(huán),而不是繼續(xù)write。另外,有時(shí)候代碼中需要連續(xù)多次調(diào)用write,可能還來不及調(diào)用read得知對(duì)方已關(guān)閉了連接就被SIGPIPE信號(hào)終止掉了,這就需要在初始化時(shí)調(diào)用sigaction處理SIGPIPE信號(hào),如果SIGPIPE信號(hào)沒有導(dǎo)致進(jìn)程異常退出,write返回-1并且errno為EPIPE。
另外,我們需要修改server,使它可以多次處理同一客戶端的請(qǐng)求。
/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len);
while (1) {
n = Read(connfd, buf, MAXLINE);
if (n == 0) {
printf("the other side has been closed.\n");
break;
}
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
Write(connfd, buf, n);
}
Close(connfd);
}
}
經(jīng)過上面的修改后,客戶端和服務(wù)器可以進(jìn)行多次交互了。我們知道,服務(wù)器通常是要同時(shí)服務(wù)多個(gè)客戶端的,運(yùn)行上面的server和client之后,再開一個(gè)終端運(yùn)行client試試,新的client能得到服務(wù)嗎?想想為什么。
2.4. 使用fork并發(fā)處理多個(gè)client的請(qǐng)求
怎么解決這個(gè)問題?網(wǎng)絡(luò)服務(wù)器通常用fork來同時(shí)服務(wù)多個(gè)客戶端,父進(jìn)程專門負(fù)責(zé)監(jiān)聽端口,每次accept一個(gè)新的客戶端連接就fork出一個(gè)子進(jìn)程專門服務(wù)這個(gè)客戶端。但是子進(jìn)程退出時(shí)會(huì)產(chǎn)生僵尸進(jìn)程,父進(jìn)程要注意處理SIGCHLD信號(hào)和調(diào)用wait清理僵尸進(jìn)程。
以下給出代碼框架,完整的代碼請(qǐng)讀者自己完成。
listenfd = socket(...);
bind(listenfd, ...);
listen(listenfd, ...);
while (1) {
connfd = accept(listenfd, ...);
n = fork();
if (n == -1) {
perror("call to fork");
exit(1);
} else if (n == 0) {
close(listenfd);
while (1) {
read(connfd, ...);
...
write(connfd, ...);
}
close(connfd);
exit(0);
} else
close(connfd);
}
2.5. setsockopt
現(xiàn)在做一個(gè)測(cè)試,首先啟動(dòng)server,然后啟動(dòng)client,然后用Ctrl-C使server終止,這時(shí)馬上再運(yùn)行server,結(jié)果是:
$ ./server
bind error: Address already in use
這是因?yàn)?,雖然server的應(yīng)用程序終止了,但TCP協(xié)議層的連接并沒有完全斷開,因此不能再次監(jiān)聽同樣的server端口。我們用netstat命令查看一下:
$ netstat -apn |grep 8000
tcp        1      0 127.0.0.1:33498         127.0.0.1:8000          CLOSE_WAIT 10830/client
tcp        0      0 127.0.0.1:8000          127.0.0.1:33498         FIN_WAIT2  -
server終止時(shí),socket描述符會(huì)自動(dòng)關(guān)閉并發(fā)FIN段給client,client收到FIN后處于CLOSE_WAIT狀態(tài),但是client并沒有終止,也沒有關(guān)閉socket描述符,因此不會(huì)發(fā)FIN給server,因此server的TCP連接處于FIN_WAIT2狀態(tài)。
現(xiàn)在用Ctrl-C把client也終止掉,再觀察現(xiàn)象:
$ netstat -apn |grep 8000
tcp        0      0 127.0.0.1:8000          127.0.0.1:44685         TIME_WAIT  -
$ ./server
bind error: Address already in use
&am, p;nb, sp;   client終止時(shí)自動(dòng)關(guān)閉socket描述符,server的TCP連接收到client發(fā)的FIN段后處于TIME_WAIT狀態(tài)。TCP協(xié)議規(guī)定,主動(dòng)關(guān)閉連接的一方要處于TIME_WAIT狀態(tài),等待兩個(gè)MSL(maximum segment lifetime)的時(shí)間后才能回到CLOSED狀態(tài),因?yàn)槲覀兿菴trl-C終止了server,所以server是主動(dòng)關(guān)閉連接的一方,在TIME_WAIT期間仍然不能再次監(jiān)聽同樣的server端口。MSL在RFC1122中規(guī)定為兩分鐘,但是各操作系統(tǒng)的實(shí)現(xiàn)不同,在Linux上一般經(jīng)過半分鐘后就可以再次啟動(dòng)server了。至于為什么要規(guī)定TIME_WAIT的時(shí)間請(qǐng)讀者參考UNP 2.7節(jié)。
在server的TCP連接沒有完全斷開之前不允許重新監(jiān)聽是不合理的,因?yàn)?,TCP連接沒有完全斷開指的是connfd(127.0.0.1:8000)沒有完全斷開,而我們重新監(jiān)聽的是listenfd(0.0.0.0:8000),雖然是占用同一個(gè)端口,但I(xiàn)P地址不同,connfd對(duì)應(yīng)的是與某個(gè)客戶端通訊的一個(gè)具體的IP地址,而listenfd對(duì)應(yīng)的是wildcard address。解決這個(gè)問題的方法是使用setsockopt()設(shè)置socket描述符的選項(xiàng)SO_REUSEADDR為1,表示允許創(chuàng)建端口號(hào)相同但I(xiàn)P地址不同的多個(gè)socket描述符。在server代碼的socket()和bind()調(diào)用之間插入如下代碼:
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
有關(guān)setsockopt可以設(shè)置的其它選項(xiàng)請(qǐng)參考UNP第7章。
2.6. 使用select
select是網(wǎng)絡(luò)程序中很常用的一個(gè)系統(tǒng)調(diào)用,它可以同時(shí)監(jiān)聽多個(gè)阻塞的文件描述符(例如多個(gè)網(wǎng)絡(luò)連接),哪個(gè)有數(shù)據(jù)到達(dá)就處理哪個(gè),這樣,不需要fork和多進(jìn)程就可以實(shí)現(xiàn)并發(fā)服務(wù)的server。
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char **argv)
{
int i, maxi, maxfd, listenfd, connfd, sockfd;
int nready, client[FD_SETSIZE];
ssize_t n;
fd_set rset, allset;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
socklen_t cliaddr_len;
struct sockaddr_in     cliaddr, servaddr;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family      = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port        = htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
Listen(listenfd, 20);
maxfd = listenfd;              /* initialize */
maxi = -1;                     /* index into client[] array */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* -1 indicates available entry */
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for ( ; ; ) {
rset = allset; /* structure assignment */
nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* new client connection */
cliaddr_len = sizeof(cliaddr);
connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) {
client[i] = connfd; /* save descriptor */
break;
}
if (i == FD_SETSIZE) {
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset);       /* add new descriptor to set */
if (connfd > maxfd)
maxfd = connfd; /* for select */
if (i > maxi)
maxi = i;      /* max index in client[] array */
if (--nready == 0)
continue;      /* no more readable descriptors */
}
for (i = 0; i <= maxi; i++) {  /* check all clients for data */
if ( (sockfd = client[i]) < 0)
continue;
if (FD_ISSET(sockfd, &rset)) {
if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {
/* connection closed by client */
Close(sockfd);
FD_CLR(sockfd, &allset);
client[i] = -1;
} else {
int j;
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
}
if (--nready == 0)
break;  /* no more readable descriptors */
}
}
}
}
3. 基于UDP協(xié)議的網(wǎng)絡(luò)程序
下圖是典型的UDP客戶端/服務(wù)器通訊過程(該圖出自[UNPv13e])。
圖 37.3. UDP通訊流程
以下是簡(jiǎn)單的UDP服務(wù)器和客戶端程序。
/* server.c */
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(void)
{
struct sockaddr_in servaddr, cliaddr;
socklen_t cliaddr_len;
int sockfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
Bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
printf("Accepting connections ...\n");
while (1) {
cliaddr_len = sizeof(cliaddr);
n = recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &cliaddr_len);
if (n == -1)
perr_exit("recvfrom error");
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
for (i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
if (n == -1)
perr_exit("sendto error");
}
}
/* client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include "wrap.h"
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
int sockfd, n;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
socklen_t servaddr_len;
sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
while (fgets(buf, MAXLINE, stdin) != NULL) {
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
if (n == -1)
perr_exit("sendto error");
n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);
if (n == -1)
perr_exit("recvfrom error");
Write(STDOUT_FILENO, buf, n);
}
Close(sockfd);
return 0;
}
由于UDP不需要維護(hù)連接,程序邏輯簡(jiǎn)單了很多,但是UDP協(xié)議是不可靠的,實(shí)際上有很多保證通訊可靠性的機(jī)制需要在應(yīng)用
層實(shí)現(xiàn)。
編譯運(yùn)行server,在兩個(gè)終端里各開一個(gè)client與server交互,看看server是否具有并發(fā)服務(wù)的能力。用Ctrl+C關(guān)閉
server,然后再運(yùn)行server,看此時(shí)client還能否和server聯(lián)系上。和前面TCP程序的運(yùn)行結(jié)果相比較,體會(huì)無連接的含義。
由于UDP不需要維護(hù)連接,程序邏輯簡(jiǎn)單了很多,但是UDP協(xié)議是不可靠的,實(shí)際上有很多保證通訊可靠性的機(jī)制需要在應(yīng)用
層實(shí)現(xiàn)。編譯運(yùn)行server,在兩個(gè)終端里各開一個(gè)client與server交互,看看server是否具有并發(fā)服務(wù)的能力。用Ctrl+C關(guān)
閉server,然后再運(yùn)行server,看此時(shí)client還能否和server聯(lián)系上。和前面TCP程序的運(yùn)行結(jié)果相比較,體會(huì)無連接的含義。
5. 練習(xí):實(shí)現(xiàn)簡(jiǎn)單的Web服務(wù)器
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的Web服務(wù)器myhttpd。服務(wù)器程序啟動(dòng)時(shí)要讀取配置文件/etc/myhttpd.conf,其中需要指定服務(wù)器監(jiān)聽的
端口號(hào)和服務(wù)目錄,例如:
Port=80
Directory=/var/www
注意,1024以下的端口號(hào)需要超級(jí)用戶才能開啟服務(wù)。如果你的系統(tǒng)中已經(jīng)安裝了某種Web服務(wù)器(例如Apache),應(yīng)該
為myhttpd選擇一個(gè)不同的端口號(hào)。當(dāng)瀏覽器向服務(wù)器請(qǐng)求文件時(shí),服務(wù)器就從服務(wù)目錄(例如/var/www)中找出這個(gè)文
件,加上HTTP協(xié)議頭一起發(fā)給瀏覽器。但是,如果瀏覽器請(qǐng)求的文件是可執(zhí)行的則稱為CGI程序,服務(wù)器并不是將這個(gè)文件
發(fā)給瀏覽器,而是在服務(wù)器端執(zhí)行這個(gè)程序,將它的標(biāo)準(zhǔn)輸出發(fā)給瀏覽器,服務(wù)器不發(fā)送完整的HTTP協(xié)議頭,CGI程序自己
負(fù)責(zé)輸出一部分HTTP協(xié)議頭。
5.1. 基本HTTP協(xié)議
打開瀏覽器,輸入服務(wù)器IP,例如 http://192.168.0.3 ,如果端口號(hào)不是80,例如是8000,則輸入 http://192.168.0.3:8000 。
這時(shí)瀏覽器向服務(wù)器發(fā)送的HTTP協(xié)議頭如下:
GET / HTTP/1.1
Host: 192.168.0.3:8000
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.6) Gecko/20061201 Firefox/2.0.0.6 (Ubuntu-feisty)
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
注意,其中每一行的末尾都是回車加換行(C語言的"\r\n"),第一行是GET請(qǐng)求和協(xié)議版本,其余幾行選項(xiàng)字段我們不討論,
HTTP協(xié)議頭的最后有一個(gè)空行,也是回車加換行。
我們實(shí)現(xiàn)的Web服務(wù)器只要能正確解析第一行就行了,這是一個(gè)GET請(qǐng)求,請(qǐng)求的是服務(wù)目錄的根目錄/(在本例中實(shí)際上
是/var/www),Web服務(wù)器應(yīng)該把該目錄下的索引頁(默認(rèn)是index.html)發(fā)給瀏覽器,也就是把/var/www/index.html
發(fā)給瀏覽器。假如該文件的內(nèi)容如下(HTML文件沒必要以"\r\n"換行,以"\n"換行就可以了):
<html>
<head><title>Test Page</title></head>
<body>
<p>Test OK</p>
<img src='mypic.jpg'>
</body>
</html>
顯示一行字和一幅圖片,圖片的相對(duì)路徑(相對(duì)當(dāng)前的index.html文件的路徑)是mypic.jpg,
也就是/var/www/mypic.jpg,
如果用絕對(duì)路徑表示應(yīng)該是:
<img src='/mypic.jpg'>
服務(wù)器應(yīng)按如下格式應(yīng)答瀏覽器:
HTTP/1.1 200 OK
Content-Type: text/html
<html>
<head><title>Test Page</title></head>
<body>
<p>Test OK</p>
<img src='mypic.jpg'>
</body>
</html>
服務(wù)器應(yīng)答的HTTP頭也是每行末尾以回車加換行結(jié)束,最后跟一個(gè)空行的回車加換行。
HTTP頭的第一行是協(xié)議版本和應(yīng)答碼,200表示成功,后面的消息OK其實(shí)可以隨意寫,瀏覽器
是不關(guān)心的,主要是為了調(diào)試時(shí)給開發(fā)人員看的。雖然網(wǎng)絡(luò)協(xié)議最終是程序與程序之間的對(duì)話,但
是在開發(fā)過程中卻是人與程序之間的對(duì)話,一個(gè)設(shè)計(jì)透明的網(wǎng)絡(luò)協(xié)議可以提供很多直觀的信息給開
發(fā)人員,因此,很多應(yīng)用層網(wǎng)絡(luò)協(xié)議,如HTTP、FTP、SMTP、POP3等都是基于文本的協(xié)議,為
的是透明性(transparency)。
HTTP頭的第二行表示即將發(fā)送的文件的類型(稱為MIME類型),這里是text/html,純文本文
件是text/plain,圖片則是image/jpg、image/png等。
然后就發(fā)送文件的內(nèi)容,發(fā)送完畢之后主動(dòng)關(guān)閉連接,這樣瀏覽器就知道文件發(fā)送完了。這一點(diǎn)
比較特殊:通常網(wǎng)絡(luò)通信都是客戶端主動(dòng)發(fā)起連接,主動(dòng)發(fā)起請(qǐng)求,主動(dòng)關(guān)閉連接,服務(wù)器只是被
動(dòng)地處理各種情況,而HTTP協(xié)議規(guī)定服務(wù)器主動(dòng)關(guān)閉連接(有些Web服務(wù)器可以配置成Keep-Alive
的,我們不討論這種情況)。
瀏覽器收到index.html之后,發(fā)現(xiàn)其中有一個(gè)圖片文件,就會(huì)再發(fā)一個(gè)GET請(qǐng)求(HTTP協(xié)議頭其
余部分略):
GET /mypic.jpg HTTP/1.1
一個(gè)較大的網(wǎng)頁中可能有很多圖片,瀏覽器可能在下載網(wǎng)頁的同時(shí)就開很多線程下載圖片,因此,
'''服務(wù)器即使對(duì)同一個(gè)客戶端也需要提供并行服務(wù)的能力'''。服務(wù)器收到這個(gè)請(qǐng)求應(yīng)該把圖片發(fā)過去然
后關(guān)閉連接:
HTTP/1.1 200 OK
Content-Type: image/jpg
(這里是mypic.jpg的二進(jìn)制數(shù)據(jù))
這時(shí)瀏覽器就應(yīng)該顯示出完整的網(wǎng)頁了。
如果瀏覽器請(qǐng)求的文件在服務(wù)器上找不到,要應(yīng)答一個(gè)404錯(cuò)誤頁面,例如:
HTTP/1.1 404 Not Found
Content-Type: text/html
<html><body>request file not found</body></html>
5.2. 執(zhí)行CGI程序
如果瀏覽器請(qǐng)求的是一個(gè)可執(zhí)行文件(不管是什么樣的可執(zhí)行文件,即使是shell腳本也一樣),
那么服務(wù)器并不把這個(gè)文件本身發(fā)給瀏覽器,而是把它的執(zhí)行結(jié)果標(biāo)準(zhǔn)輸出發(fā)
給瀏覽器。例如一個(gè)shell腳本/var/www/myscript.sh(注意一定要加可執(zhí)行權(quán)限):
#!/bin/sh
echo "Content-Type: text/html"
echo
echo "<html><body>Hello world!</body></html>"
這樣瀏覽器收到的是:
HTTP/1.1 200 OK
Content-Type: text/html
<html><body>Hello world!</body></html>
總結(jié)一下服務(wù)器的處理步驟:
1.  解析瀏覽器的請(qǐng)求,在服務(wù)目錄中查找相應(yīng)的文件,如果找不到該文件就返回404錯(cuò)誤頁面
2.  如果找到了瀏覽器請(qǐng)求的文件,用stat(2)檢查它是否可執(zhí)行
3.  如果該文件可執(zhí)行:
a.  發(fā)送HTTP/1.1 200 OK給客戶端
b.  fork(2),然后用dup2(2)重定向子進(jìn)程的標(biāo)準(zhǔn)輸出到客戶端socket
c.  在子進(jìn)程中exec(3)該CGI程序
d.  關(guān)閉連接
4.  如果該文件不可執(zhí)行:
a.  發(fā)送HTTP/1.1 200 OK給客戶端
b.  如果是一個(gè)圖片文件,根據(jù)圖片的擴(kuò)展名發(fā)送相應(yīng)的Content-Type給客戶端
c.   如果不是圖片文件,這里我們簡(jiǎn)化處理,都當(dāng)作Content-Type: text/html
d.  簡(jiǎn)單的HTTP協(xié)議頭有這兩行就足夠了,再發(fā)一個(gè)空行表示結(jié)束
e.  讀取文件的內(nèi)容發(fā)送到客戶端
f.   關(guān)閉連接
本站僅提供存儲(chǔ)服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊舉報(bào)
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
socket本地進(jìn)程通信
自己動(dòng)手用c語言寫一個(gè)基于服務(wù)器和客戶端(TCP)
Linux環(huán)境下的Socket編程 - C&C - Linux技術(shù)中堅(jiān)站
socket編程結(jié)構(gòu)體變量(轉(zhuǎn))
Linux下TCP/IP編程
Linux操作系統(tǒng)下實(shí)現(xiàn)多線程客戶/服務(wù)器
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長(zhǎng)圖 關(guān)注 下載文章
綁定賬號(hào)成功
后續(xù)可登錄賬號(hào)暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服