用C編寫一個(gè)簡(jiǎn)單服務(wù)器

前言

本文使用C語言編寫一個(gè)簡(jiǎn)單服務(wù)器柄瑰,旨在更好的理解服務(wù)端/客戶端程序,迭代服務(wù)器剪况,并發(fā)服務(wù)器等概念教沾,僅供學(xué)習(xí)參考。這篇文章的例子很簡(jiǎn)單译断,就是當(dāng)客戶端連接上服務(wù)端之后授翻,服務(wù)端給出一個(gè)“Hello World”回應(yīng)。

C/S結(jié)構(gòu)流程圖

整個(gè)客戶端,服務(wù)端交互流程可以用下圖表示堪唐,服務(wù)端是優(yōu)先啟動(dòng)進(jìn)程并監(jiān)聽某一個(gè)端口巡语,并且進(jìn)程一直阻塞,直到有客戶端連接進(jìn)來淮菠,才開始處理客戶端連接男公。

image

服務(wù)端

通過流程圖可以看出,服務(wù)端涉及的Socket函數(shù)有socket, bind, listen, accept, read, write, close合陵。使用這7個(gè)函數(shù)就可以編寫出一個(gè)簡(jiǎn)易服務(wù)器枢赔。

socket函數(shù)

為了執(zhí)行網(wǎng)絡(luò)I/O,一個(gè)進(jìn)程必須做的第一件事情就是創(chuàng)建一個(gè)socket函數(shù)拥知,函數(shù)原型

# family 表示協(xié)議族
# type 表示套接字類型
# protocol 表示傳輸協(xié)議
# 若成功返回非負(fù)描述符踏拜,若出錯(cuò)返回-1
int socket(int family, int type, int protocol);

這個(gè)函數(shù)需要傳入?yún)f(xié)議族,套接字類型低剔,傳輸層協(xié)議三個(gè)參數(shù)速梗。

協(xié)議族可以有以下取值

family 說明
AF_INET IPv4協(xié)議
AF_INET6 IPv6協(xié)議
AF_LOCAL Unix域協(xié)議
AF_ROUTE 路由套接字
AF_KEY 密鑰套接字

套接字類型可以有以下取值

type 說明
SOCK_STREAM 字節(jié)流套接字
SOCK_DGRAM 數(shù)據(jù)報(bào)套接字
SOCK_SEQPACKET 有序分組套接字
SOCK_ROW 原始套接字

傳輸層協(xié)議可以有以下取值

protocol 說明
IPPROTO_TCP TCP傳輸協(xié)議
IPPROTO_UDP UDP傳輸協(xié)議
IPPROTO_SCTP SCTP傳輸協(xié)議

這里我們選擇IPv4協(xié)議,使用字節(jié)流套接字襟齿,傳輸層選擇TCP協(xié)議镀琉,所以第一段代碼:

#include <stdio.h>
#include <sys/socket.h>
int main()
{
    int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    
    if(server_sockfd == -1){
        printf("socket error");
        return -1;
    }
}

bind函數(shù)

bind函數(shù)把一個(gè)本地協(xié)議地址賦予一個(gè)套接字,對(duì)于網(wǎng)際協(xié)議蕊唐,協(xié)議地址就是IP加端口的組合,函數(shù)原型

# sockfd 初始化的套接字
# myaddr 協(xié)議地址
# addrlen 協(xié)議地址長(zhǎng)度
# 若成功返回0 出錯(cuò)返回-1
int bind(int sockfd, const struct sockaddr * myaddr, socklen_t addrlen)

注意烁设,這個(gè)函數(shù)不是必須的替梨,如果不使用這個(gè)函數(shù)綁定一個(gè)特定的端口,那么內(nèi)核會(huì)幫我們的套接字選擇一個(gè)臨時(shí)端口装黑。作為服務(wù)器副瀑,一般不會(huì)這么做,需要指定特定的端口恋谭。

這個(gè)函數(shù)的第二個(gè)參數(shù)是協(xié)議地址糠睡,注意,這個(gè)協(xié)議地址已經(jīng)有定義好的結(jié)構(gòu)體疚颊,使用IPv4套接字結(jié)構(gòu)地址時(shí)候狈孔,地址結(jié)構(gòu)體定義如下

struct sockaddr_in {
    uint8_t sin_len; /*結(jié)構(gòu)體長(zhǎng)度*/
    sa_family_t sin_family; /*AF_INET*/
    in_port_t sin_port; /*端口(16-bie)*/
    struct in_addr sin_addr; /*IPv4地址(32-bit)*/
    char sin_zero[8]; /*沒啥用,設(shè)置0即可*/
}

我們讓我們的服務(wù)器綁定8887端口(80端口被web占用了材义,用8887端口代替),所以我們的第二段代碼

#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
    int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    
    if(server_sockfd == -1){
        printf("socket error");
        return -1;
    }
    
    struct sockaddr_in server_sockaddr;/*聲明一個(gè)變量均抽,類型為協(xié)議地址類型*/
    server_sockaddr.sin_family = AF_INET;/*使用IPv4協(xié)議*/
    server_sockaddr.sin_port = htons(8887);/*監(jiān)聽8887端口*/
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*綁定本機(jī)IP,使用宏定義綁定*/

    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){
        printf("bind error");
        return -1;
    }
}

listen函數(shù)

listen函數(shù)僅有服務(wù)器調(diào)用其掂,它完成兩件事情:

  1. 當(dāng)使用socket函數(shù)創(chuàng)建一個(gè)套接字時(shí)油挥,它被假設(shè)為一個(gè)主動(dòng)套接字,也就是說,它是一個(gè)將發(fā)送connect發(fā)起連接的客戶端套接字深寥。當(dāng)調(diào)用listen函數(shù)之后攘乒,它被轉(zhuǎn)成一個(gè)被動(dòng)套接字,只是內(nèi)核應(yīng)該接受連接請(qǐng)求惋鹅。所以则酝,調(diào)用listen之后套接字由CLOSED狀態(tài)轉(zhuǎn)到LISTEN狀態(tài)
  2. 這個(gè)函數(shù)規(guī)定內(nèi)核應(yīng)該為相應(yīng)套接字排隊(duì)的最大連接數(shù)

函數(shù)原型

/*失敗時(shí)返回-1*/
int listen(int sockfd, int backlog)

backlog參數(shù)的設(shè)定其實(shí)是表示兩個(gè)隊(duì)列的總和,這兩個(gè)隊(duì)列分別是

  1. 未完成連接隊(duì)列负饲,在客戶端發(fā)送一個(gè)SYN直到三次握手完成堤魁,都是這個(gè)狀態(tài),SYN_RCVD狀態(tài)返十。
  2. 已完成連接隊(duì)列妥泉,這個(gè)表示三次握手完成的狀態(tài),ESTABLISHED狀態(tài)

因?yàn)槲覀兪菧y(cè)試洞坑,這個(gè)值設(shè)置成20就可以了盲链。所以我們的第三段代碼

#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
    int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    
    if(server_sockfd == -1){
        printf("socket error");
        return -1;
    }
    
    struct sockaddr_in server_sockaddr;/*聲明一個(gè)變量,類型為協(xié)議地址類型*/
    server_sockaddr.sin_family = AF_INET;/*使用IPv4協(xié)議*/
    server_sockaddr.sin_port = htons(8887);/*監(jiān)聽8887端口*/
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*綁定本機(jī)IP迟杂,使用宏定義綁定*/

    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){
        printf("bind error");
        return -1;
    }
    
    if(listen(server_sockfd, 20) == -1){
        printf("listen error");
        return -1;
    }

}

accept函數(shù)

accept函數(shù)是由TCP服務(wù)器調(diào)用刽沾,用于從已完成連接隊(duì)列的隊(duì)頭返回下一個(gè)已完成連接,如果已完成連接隊(duì)列為空排拷,那么進(jìn)程進(jìn)入睡眠模式侧漓,函數(shù)原型

# sockdf 服務(wù)器套接字莫描述符
# cliaddr 已連接的客戶端協(xié)議地址
# addrlen 已連接的客戶端協(xié)議地址長(zhǎng)度
# 成功返回非負(fù)描述符,出錯(cuò)返回-1
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);

當(dāng)accept成功時(shí)监氢,返回值是由內(nèi)核自動(dòng)生成的全新描述符布蔗,代表與所返回的客戶端TCP連接。所以浪腐,在我們討論accept函數(shù)時(shí)纵揍,我們稱第一個(gè)參數(shù)為監(jiān)聽套接字,它的返回值是已連接套接字议街,一個(gè)服務(wù)器通常指創(chuàng)建一個(gè)監(jiān)聽套接字(通常是80端口)泽谨,內(nèi)核為每個(gè)由服務(wù)器進(jìn)程接受的客戶端連接創(chuàng)建一個(gè)已連接套接字,當(dāng)服務(wù)器完成對(duì)某個(gè)給定的客戶端服務(wù)時(shí)特漩,連接就會(huì)被關(guān)閉吧雹。

函數(shù)的第二個(gè)參數(shù)也是一個(gè)協(xié)議地址結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體和服務(wù)端協(xié)議地址是同一個(gè)結(jié)構(gòu)體涂身。我們可以不關(guān)心客戶端的協(xié)議吮炕,直接傳空,我們關(guān)系的是這個(gè)函數(shù)的返回值访得,因?yàn)樗祷氐氖强蛻舳诉B接描述符龙亲,我們可以對(duì)這個(gè)描述符進(jìn)行寫操作陕凹,從而實(shí)現(xiàn)給客戶端傳輸數(shù)據(jù)。所以我們第四段代碼

#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
    int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    
    if(server_sockfd == -1){
        printf("socket error");
        return -1;
    }
    
    struct sockaddr_in server_sockaddr;/*聲明一個(gè)變量鳄炉,類型為協(xié)議地址類型*/
    server_sockaddr.sin_family = AF_INET;/*使用IPv4協(xié)議*/
    server_sockaddr.sin_port = htons(8887);/*監(jiān)聽8887端口*/
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*綁定本機(jī)IP杜耙,使用宏定義綁定*/
    
    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){
        printf("bind error");
        exit(1);
    }
    
    if(listen(server_sockfd, 20) == -1){
        printf("listen error");
        exit(1);
    }
    
    struct sockaddr_in clnt_addr;/*只是聲明,并沒有賦值*/
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    
    if(clnt_sock == -1){
        printf("appect error");
        return -1;
    }
    
}

write函數(shù)

前面的操作都完成之后拂盯,說明服務(wù)端和客戶端已經(jīng)建立連接佑女,由于TCP的傳輸是全雙工的,這時(shí)候客戶端和服務(wù)端都可以向?qū)Ψ桨l(fā)送數(shù)據(jù)谈竿。這里為了簡(jiǎn)化团驱,我們實(shí)現(xiàn)服務(wù)端發(fā)送“Hello World”給請(qǐng)求連接的客戶端。給客戶端發(fā)送數(shù)據(jù)很簡(jiǎn)單空凸,就是對(duì)返回的客戶端描述符進(jìn)行寫操作就可以了

# sockfd socket文件描述符
# buf 文件內(nèi)容
# count 內(nèi)容長(zhǎng)度
ssize_t write(int sockfd, const void * buf, size_t count);

完整的服務(wù)器代碼

#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
    int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    
    if(server_sockfd == -1){
        printf("socket error");
        return -1;
    }
    
    struct sockaddr_in server_sockaddr;/*聲明一個(gè)變量嚎花,類型為協(xié)議地址類型*/
    server_sockaddr.sin_family = AF_INET;/*使用IPv4協(xié)議*/
    server_sockaddr.sin_port = htons(8887);/*監(jiān)聽8887端口*/
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*綁定本機(jī)IP,使用宏定義綁定*/
    
    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){
        printf("bind error");
        return -1;
    }
    
    if(listen(server_sockfd, 20) == -1){
        printf("listen error");
        return -1;
    }
    
    struct sockaddr_in clnt_addr;/*只是聲明呀洲,并沒有賦值*/
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    
    if(clnt_sock == -1){
        printf("appect error");
        return -1;
    }
    
    char str[] = "Hello World";
    write(clnt_sock, str, sizeof(str));
    
    close(clnt_sock);
    close(server_sockfd);
}

客戶端

客戶端要和服務(wù)器進(jìn)行通信紊选,從流程圖上可以看出,需要使用socket, connect, write, read, close這5個(gè)函數(shù)

socket函數(shù)

客戶端要和服務(wù)端進(jìn)行網(wǎng)絡(luò)通訊道逗,首先也必須調(diào)用socket函數(shù)兵罢,這里客戶端也使用IPv4協(xié)議,使用字節(jié)流套接字滓窍,傳輸層選擇TCP協(xié)議卖词,所以第一段代碼

#include <stdio.h>
#include <sys/socket.h>
int main()
{
    int sock_cli = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    
    if(sock_cli == -1){
        printf("socket error");
        return -1;
    }
}

connect函數(shù)

TCP客戶端就是使用connect函數(shù)和服務(wù)端建立連接,函數(shù)原型

# sockfd 客戶端TCP描述符
# sockaddr 服務(wù)端協(xié)議地址
# addrlen 服務(wù)端協(xié)議地址長(zhǎng)度
int connect(int sockfd, const struct sockaddr * servaddr, socklen_t addrlen);

這個(gè)函數(shù)將觸發(fā)客戶端和服務(wù)端三次握手吏夯,函數(shù)的第一個(gè)參數(shù)sockfd表示客戶端返回的描述符坏平,這里不需要調(diào)用bind函數(shù)綁定端口,系統(tǒng)會(huì)自動(dòng)分配锦亦。函數(shù)的第二個(gè)參數(shù)需要配置服務(wù)端IP和端口信息,同樣有結(jié)構(gòu)體規(guī)范這些信息令境,結(jié)構(gòu)體也是和服務(wù)端一樣使用sockaddr_in類型杠园。所以第二段代碼

#include <stdio.h>
#include <sys/socket.h>
int main()
{
    int sock_cli = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    
    if(sock_cli == -1){
        printf("socket error");
        return -1;
    }
    
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;/*使用IPv4協(xié)議*/
    servaddr.sin_port = htons(8887);/*需要連接的遠(yuǎn)程服務(wù)器端口*/
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");/*需要連接的遠(yuǎn)程服務(wù)器IP*/
    
    if(connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr))  == -1){
        printf("connect error");
        return -1;
    }

}

read函數(shù)

客戶端連接上服務(wù)器之后返回的是一個(gè)Socket文件描述符,既然是文件描述符舔庶,就可以通過簡(jiǎn)單的read函數(shù)獲取網(wǎng)絡(luò)數(shù)據(jù)抛蚁,read函數(shù)原型

# sockdf 文件描述符
# buf 文件內(nèi)容存放地址
# count 內(nèi)容長(zhǎng)度
ssize_t read(int sockfd,void *buf,size_t count)

這里我們讀取64個(gè)字節(jié)就夠了,不需要太多

char str[64];
read(sock_cli, str, 64);

完整的客戶端代碼

#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
    int sock_cli = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    
    if(sock_cli == -1){
        printf("socket error");
        return -1;
    }
    
    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;/*使用IPv4協(xié)議*/
    servaddr.sin_port = htons(8887);/*需要連接的遠(yuǎn)程服務(wù)器端口*/
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");/*需要連接的遠(yuǎn)程服務(wù)器IP*/
    
    if(connect(sock_cli, (struct sockaddr *)&servaddr, sizeof(servaddr))  == -1){
        printf("connect error");
        return -1;
    }

    char str[64];
    read(sock_cli, str, 64);
    printf(str);

    close(sock_cli);
}

運(yùn)行客戶端服務(wù)端

將我們的服務(wù)端代碼保存為server.c惕橙,將我們的客戶端代碼保存為client.c瞧甩。分別編譯客戶端和服務(wù)端代碼

[root@iZ940ofmvruZ socket]# gcc server.c -o server
[root@iZ940ofmvruZ socket]# gcc client.c -o client

然后會(huì)分別生成兩個(gè)可執(zhí)行文件server和client。在一個(gè)窗口中先執(zhí)行server

[root@iZ940ofmvruZ socket]# ./server


執(zhí)行server之后弥鹦,我們知道accept函數(shù)會(huì)阻塞肚逸,所以程序一直運(yùn)行爷辙,等待客戶端連接進(jìn)來。這時(shí)候在另一個(gè)窗口執(zhí)行客戶端

[root@iZ940ofmvruZ socket]# ./client 
Hello World

可以看到服務(wù)端給我們發(fā)送的Hello World朦促,我們?cè)倩氐椒?wù)端執(zhí)行窗口時(shí)膝晾,服務(wù)端也終止了進(jìn)程,這個(gè)交互完成务冕。然后我們會(huì)發(fā)現(xiàn)一個(gè)問題血当,服務(wù)端在提供完服務(wù)之后,它自己也關(guān)閉了禀忆,很顯然臊旭,我們希望服務(wù)器是一致運(yùn)行的提供服務(wù),所以我們需要實(shí)現(xiàn)一直運(yùn)行的服務(wù)器

不間斷提供服務(wù)

讓服務(wù)器一直運(yùn)行的方式很簡(jiǎn)單箩退,就是死循環(huán)离熏。循環(huán)的過程是accept一個(gè)客戶端連接,然后處理數(shù)據(jù)請(qǐng)求乏德,最后關(guān)閉客戶端連接撤奸。注意,我們不能關(guān)閉服務(wù)端連接喊括。所以我們改進(jìn)這個(gè)程序胧瓜,讓server不間斷的調(diào)用accept,因?yàn)閍ccept總是從已連接的隊(duì)列中返回一個(gè)連接郑什,然后處理府喳。改進(jìn)內(nèi)容片斷

/**
 * 進(jìn)入死循環(huán)調(diào)用accept,給每一個(gè)連接上來的客戶端發(fā)送Hello World
 */
for( ; ; ){
    struct sockaddr_in clnt_addr;/*只是聲明蘑拯,并沒有賦值*/
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

    if(clnt_sock == -1){
        printf("appect error");
        return -1;
    }

    char str[] = "Hello World";
    write(clnt_sock, str, sizeof(str));

    close(clnt_sock);
    /*close(server_sockfd);*/
}

這樣修改之后钝满,這個(gè)服務(wù)端程序就是一直不間斷提供服務(wù)了

并發(fā)服務(wù)器

迭代服務(wù)器

我們首先來看下什么是迭代服務(wù)器,因?yàn)槲覀儎偛潘鶎懙木褪且粋€(gè)迭代服務(wù)器申窘,思考一個(gè)問題弯蚜,假如我們的服務(wù)器不是輸出Hello World這么簡(jiǎn)單,而是需要經(jīng)過一系列復(fù)雜邏輯計(jì)算甚至網(wǎng)絡(luò)調(diào)用剃法,那我們的程序執(zhí)行起來就不會(huì)怎么快了碎捺,為了模擬這種場(chǎng)景,我們?cè)诜?wù)端程序中假如sleep函數(shù)贷洲,我們讓程序睡眠3秒鐘收厨,模擬服務(wù)器處理復(fù)雜邏輯時(shí)間

#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
    int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    
    if(server_sockfd == -1){
        printf("socket error");
        return -1;
    }
    
    struct sockaddr_in server_sockaddr;/*聲明一個(gè)變量,類型為協(xié)議地址類型*/
    server_sockaddr.sin_family = AF_INET;/*使用IPv4協(xié)議*/
    server_sockaddr.sin_port = htons(8887);/*監(jiān)聽8887端口*/
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*綁定本機(jī)IP优构,使用宏定義綁定*/
    
    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){
        printf("bind error");
        return -1;
    }
    
    if(listen(server_sockfd, 20) == -1){
        printf("listen error");
        return -1;
    }

    for( ; ; ){    
        struct sockaddr_in clnt_addr;/*只是聲明诵叁,并沒有賦值*/
        socklen_t clnt_addr_size = sizeof(clnt_addr);
        int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
        
        if(clnt_sock == -1){
            printf("appect error");
            return -1;
        }
        
        char str[] = "Hello World";
        sleep(3);//3秒之后再向客戶端發(fā)送數(shù)據(jù)
        write(clnt_sock, str, sizeof(str));
    
        close(clnt_sock);
        /*close(server_sockfd);*/
    }
}

運(yùn)行這個(gè)服務(wù)端程序之后,我們同時(shí)執(zhí)行10個(gè)客戶端

for(( i=0; i< 10; i++ ))
    do
    {
        ./client
    }&
done

在shell中執(zhí)行這段代碼钦椭,你會(huì)發(fā)現(xiàn)每隔3秒鐘輸出一個(gè)Hello World拧额。這是因?yàn)槲覀兊姆?wù)端程序是阻塞的碑诉,在處理一個(gè)請(qǐng)求的同時(shí),其他請(qǐng)求只能等势腮。所以最后一個(gè)客戶端連接需要等到30秒才能收到服務(wù)端的輸出联贩。我們稱這種服務(wù)器為迭代服務(wù)器,迭代服務(wù)器會(huì)依次處理客戶端的連接捎拯,只要當(dāng)前連接的任務(wù)沒有完成泪幌,服務(wù)器的進(jìn)程就會(huì)一直被占用师倔,直到任務(wù)完成后袖裕,服務(wù)器關(guān)閉這個(gè)socket鼠渺,釋放連接萝嘁。這顯然不是我們想要的暴拄,我們希望每一個(gè)Hello Wrold都在3秒鐘后馬上輸出啸臀。

并發(fā)服務(wù)器

當(dāng)一個(gè)服務(wù)處理客戶端請(qǐng)求需要花費(fèi)較長(zhǎng)時(shí)間烹俗,但是我們又不希望整個(gè)服務(wù)器被單個(gè)客戶端長(zhǎng)期占用窘奏,而是希望同時(shí)服務(wù)多個(gè)客戶禁荸。Unix中編寫并發(fā)服務(wù)器最簡(jiǎn)單的辦法就是fork一個(gè)子進(jìn)程來服務(wù)每個(gè)客戶右蒲。利用fork函數(shù)可以把處理客戶端請(qǐng)求的任務(wù)交接到子進(jìn)程,這樣就實(shí)現(xiàn)多進(jìn)程并發(fā)赶熟,我們可以寫出這樣服務(wù)器的輪廓

pid_t pid;
for( ; ; ){    
    struct sockaddr_in clnt_addr;/*只是聲明瑰妄,并沒有賦值*/
    socklen_t clnt_addr_size = sizeof(clnt_addr);
    int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    
    if(clnt_sock == -1){
        printf("appect error");
        return -1;
    }
    
    /**
     * 這一段直接fork一個(gè)子進(jìn)程
     * 子進(jìn)程處理單獨(dú)處理完請(qǐng)求之后退出
     */
    if( (pid = fork()) == 0 ){
        close(server_sockfd);/*子進(jìn)程不需要監(jiān)聽,關(guān)閉*/
        doit(clnt_sock);/*針對(duì)已連接的客戶端套接字進(jìn)行讀寫*/
        close(clnt_sock);/*處理完畢映砖,關(guān)閉客戶端連接*/
        exit(0);/*自覺退出*/
    }
    
    close(clnt_sock); /*連接已經(jīng)交由子進(jìn)程處理间坐,父進(jìn)程可以關(guān)閉客戶端連接了*/
    /*close(server_sockfd);*/
}

其中,doit函數(shù)我們先不實(shí)現(xiàn)邑退,我們來看一下一個(gè)并發(fā)服務(wù)器處理一個(gè)客戶端連接的流程

  1. 服務(wù)器阻塞于accept調(diào)用且來自客戶的連接請(qǐng)求到達(dá)時(shí)的客戶端與服務(wù)器的狀態(tài)
image
  1. 從accept返回后竹宋,連接已經(jīng)在內(nèi)核中注冊(cè),并且新的套接口connfd被創(chuàng)建地技。這是一個(gè)已建起連接的套接口蜈七,可以進(jìn)行數(shù)據(jù)的讀寫。
image

3.并發(fā)服務(wù)器在調(diào)用fork之后莫矗,listenfd和connfd這兩個(gè)描述字在父進(jìn)程以及子進(jìn)程之間共享(實(shí)際為其中一份為copy)

image
  1. 接下來是由父進(jìn)程關(guān)閉已連接套接口(connfd)飒硅,由子進(jìn)程關(guān)閉監(jiān)聽套接口(listenfd)。然后由子進(jìn)程負(fù)責(zé)為客戶端提供服務(wù)
image

最終我們的并發(fā)服務(wù)器代碼為

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>

void doit(int sockfd);

int main()
{
    int server_sockfd = socket(AF_INET,SOCK_STREAM, IPPROTO_TCP);
    pid_t pid;
    
    if(server_sockfd == -1){
        printf("socket error");
        return -1;
    }
    
    struct sockaddr_in server_sockaddr;/*聲明一個(gè)變量趣苏,類型為協(xié)議地址類型*/
    server_sockaddr.sin_family = AF_INET;/*使用IPv4協(xié)議*/
    server_sockaddr.sin_port = htons(8887);/*監(jiān)聽8887端口*/
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);/*綁定本機(jī)IP,使用宏定義綁定*/
    
    if(bind(server_sockfd,(struct sockaddr *)&server_sockaddr,sizeof(server_sockaddr))==-1){
        printf("bind error");
        return -1;
    }
    
    if(listen(server_sockfd, 20) == -1){
        printf("listen error");
        return -1;
    }

    for( ; ; ){    
        struct sockaddr_in clnt_addr;/*只是聲明梯轻,并沒有賦值*/
        socklen_t clnt_addr_size = sizeof(clnt_addr);
        int clnt_sock = accept(server_sockfd, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    
        if(clnt_sock == -1){
            printf("appect error");
            return -1;
        }

        if( (pid = fork()) == 0 ){
            close(server_sockfd);/*子進(jìn)程不需要監(jiān)聽食磕,關(guān)閉*/
            doit(clnt_sock);/*針對(duì)已連接的客戶端套接字進(jìn)行讀寫*/
            close(clnt_sock);/*處理完畢,關(guān)閉客戶端連接*/
            exit(0);/*自覺退出*/
        }    

        close(clnt_sock);
        /*close(server_sockfd);*/
    }
}

void doit(int sockfd){
    char str[] = "Hello World";
    sleep(3);//3秒之后再向客戶端發(fā)送數(shù)據(jù)
    write(sockfd, str, sizeof(str));
}

這個(gè)時(shí)候再次利用shell并行執(zhí)行我們的客戶端喳挑,就會(huì)發(fā)現(xiàn)彬伦,所有的Hello World是同時(shí)輸出來的滔悉。這種服務(wù)器就可以做到快速同時(shí)處理多個(gè)客戶端連接。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末单绑,一起剝皮案震驚了整個(gè)濱河市回官,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌搂橙,老刑警劉巖歉提,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異区转,居然都是意外死亡苔巨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門废离,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侄泽,“玉大人,你說我怎么就攤上這事蜻韭〉课玻” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵肖方,是天一觀的道長(zhǎng)闺魏。 經(jīng)常有香客問我,道長(zhǎng)窥妇,這世上最難降的妖魔是什么舷胜? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮活翩,結(jié)果婚禮上烹骨,老公的妹妹穿的比我還像新娘。我一直安慰自己材泄,他們只是感情好沮焕,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拉宗,像睡著了一般峦树。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旦事,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天魁巩,我揣著相機(jī)與錄音,去河邊找鬼姐浮。 笑死谷遂,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的卖鲤。 我是一名探鬼主播肾扰,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼畴嘶,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了集晚?” 一聲冷哼從身側(cè)響起窗悯,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎偷拔,沒想到半個(gè)月后蒋院,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡条摸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年悦污,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钉蒲。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡切端,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出顷啼,到底是詐尸還是另有隱情踏枣,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布钙蒙,位于F島的核電站茵瀑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏躬厌。R本人自食惡果不足惜马昨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扛施。 院中可真熱鬧鸿捧,春花似錦、人聲如沸疙渣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妄荔。三九已至泼菌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間啦租,已是汗流浹背哗伯。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留篷角,地道東北人焊刹。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親伴澄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理阱缓,服務(wù)發(fā)現(xiàn)非凌,斷路器,智...
    卡卡羅2017閱讀 134,599評(píng)論 18 139
  • 參考:http://www.2cto.com/net/201611/569006.html TCP HTTP UD...
    F麥子閱讀 2,939評(píng)論 0 14
  • 1喉悴、TCP狀態(tài)linux查看tcp的狀態(tài)命令:1)、netstat -nat 查看TCP各個(gè)狀態(tài)的數(shù)量2)玖媚、lso...
    北辰青閱讀 9,398評(píng)論 0 11
  • 最近在看《UNIX網(wǎng)絡(luò)編程 卷1》和《FREEBSD操作系統(tǒng)設(shè)計(jì)與實(shí)現(xiàn)》這兩本書箕肃,我重點(diǎn)關(guān)注了TCP協(xié)議相關(guān)的內(nèi)容...
    腩啵兔子閱讀 1,151評(píng)論 0 7
  • 總結(jié)一些常用的銜接詞,雖然不能涵蓋所今魔,但如果把這十類詞的關(guān)系搞清楚勺像,把列舉的具體銜接詞背誦并會(huì)用的話,就能把一篇文...
    張簡(jiǎn)亦閱讀 8,463評(píng)論 0 1