unix網絡編程(1)---客戶端-服務器初版

我的認爲《Unix網絡編程》前4章能夠好好看幾遍,不用先着急編程。另外做者提供的源碼封裝過重,不如本身基於原始庫函數編寫客戶端以及服務器,目前一些開源的項目也都是基於這些基礎庫函數的。編程

在瞭解了前四章的主要知識點後,好比socket、bind、connect、listen、accept等函數後,對網絡編程有了必定的瞭解後,就能夠參考第5章來寫本身的客戶端和服務器了。對於新手來講這裏比較抽象,並且不少地方繞來繞去容易繞暈,須要重複看屢次,再看後邊的章節。服務器

這篇文章我就從第5章開始,仿照書上的demo寫一個能夠直接在單機上運行的cli-ser程序。網絡

 

如下是server的對應程序:server.c併發

 1 #include <unistd.h>
 2 #include <stdlib.h>
 3 #include <errno.h>
 4 
 5 #define MAXLINE 1024
 6 
 7 extern int errno;
 8 
 9 void str_echo(int);
10 
11 int main() {
12     int sockfd;
13     sockfd = socket(AF_INET, SOCK_STREAM, 0);
14 
15     struct sockaddr_in servaddr, cliaddr;
16     bzero(&servaddr, sizeof(servaddr));
17     servaddr.sin_family = AF_INET;
18     servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
19     servaddr.sin_port = htons(7070);
20     bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
21     listen(sockfd, 1024);
22 
23     for (;;) {
24         int connfd, childPid;
25         socklen_t len = sizeof(cliaddr);
26         connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
27 
28         if ((childPid = fork()) == 0) {
29             close(sockfd);
30             printf("connected with client.\n");
31             str_echo(connfd);
32             exit(0);
33         }
34     }
35 
36     printf("server end!\n");
37     return 0;
38 }
39 
40 void str_echo(int sockfd) {
41     ssize_t n;
42     char buf[MAXLINE];
43 
44 again:
45 
46     while ((n = read(sockfd, buf, MAXLINE)) > 0) {
47         printf("n:%ld\n", n);
48         write(sockfd, buf, n);
49         bzero(buf, MAXLINE);
50 
51         if (n < 0 && errno == EINTR) {
52             goto again;
53         } else if (n < 0) {
54             printf("str_echo:read error\n");
55         }
56     }
57 }

編譯:gcc server.c -o serversocket

這裏先列下常常用到的網絡字段類型:tcp

代碼流程:函數

一、申請socketspa

服務器首先申請socket,socket相似於再Unix系統上打開一個文件,會返回一個文件標識號用來標識當前打開的文件。指針

socket須要引用<sys/socket.h>頭文件code

int socket(int family, int type, int protocol);

family:對應的是協議族,ipv4:AF_INET   ipv6:AF_INET6

type:套接字類型,tcp對應SOCKET_STREAM(數據流)

protocol:協議類型,這裏咱們用0,內核會根據family和type選擇默認的協議,對於family:AF_INET,type:SOCK_STREAM,默認的協議是tcp

二、端口綁定

通常服務器啓動一個服務進程會開啓某個端口的監聽工做,因此通常的服務器進程須要綁定固定的端口號,也就是該進程對應的socket須要綁定到某一個端口號。對於多網卡的服務器,會對應多個ip,固然也能夠綁定固定的ip,咱們這裏不進行綁定 ,使用通配地址(ipv4:INADDR_ANY,ipv6:IN6ADDR_ANY_INIT),此處的端口或ip綁定用的函數是bind

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

sockfd:監聽套接字,對於服務器來講,即調socket返回的套接字

myaddr:套接字結構體,咱們通常會先申請一個sockaddr_in結構的套接字,經過bzero函數(string.h的一個函數)進行結構體初始化爲0,分別對family,ip,port填值,而後用sockaddr強制類型轉化進行調用,具體的能夠參考書中bind函數使用;

addrlen:爲套接字結構體長度

三、套接字端口監聽

目前已經在申請好的套接字上進行了監聽ip及port的初始化,那麼能夠內核開始按照咱們初始化的信息進行監聽了,即調用listen函數,內核會申請一個隊列用於存放未完成鏈接以及已完成鏈接的套接字,以下圖

映射到tcp的三次握手,以下圖:

 

四、與客戶端創建鏈接

下邊咱們會進入一個無限循環,會一直處理client發來的tcp連接,accept爲阻塞函數,若是沒有客戶端鏈接,這個函數會被阻塞,也就是程序會在這裏中止,知道有client創建了tcp鏈接accept才返回,accept返回也就說明,此時已經創建好一條tcp鏈接通路,下邊咱們的服務器會在這條通路上進行數據的發送與接收,至於接收後會怎麼處理,以及返回客戶端什麼數據,就屬於服務器本身的業務需求了。咱們這裏會fork一個子進程進行這些邏輯的處理。爲何要創建子進程呢?咱們的服務器進程是併發的服務器,若是accept後,進程開始處理業務邏輯,那麼其餘的client須要等待這條tcp完成邏輯處理後,才能進入下一次循環。因此咱們新建子進程專門用於邏輯的處理,至於父進程就專門負責accept,創建新的連接,這樣多個client發起與服務器的tcp連接,服務器主進程能夠一直循環accept創建鏈接,而後fork子進程進行後續處理,這樣咱們就實現了簡單的併發服務器,能夠同時與多個client創建tcp鏈接。

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); 

這裏有一點須要注意,addrlen使用的是指針,這是因爲addrlen的入參會被內核使用到,已提醒讀取cliaddr的長度,另外,內核會寫回cliaddr,這也防止內存溢出,而且寫入多少這個數,內核還會寫回addrlen,這裏一個參數作了多個事情,因此用了值—參數這種指針傳參。

 

#include <unistd.h>

pid_t fork(void);

建立子進程,對於父進程返回值爲子進程的進程id,對於子進程返回0。

 

對於server中用到的read和write函數,參考Unix高級編程中的相關知識。

 

如下是client代碼:client.c

 1 #include <sys/socket.h>
 2 #include <netinet/in.h>
 3 #include <stdio.h>
 4 #include <string.h>
 5 #include <arpa/inet.h>
 6 #include <unistd.h>
 7 #include <unistd.h>
 8 
 9 #define MAXLINE 1024
10 
11 void str_cli(FILE *, int);
12 
13 int main() {
14     int sockfd;
15     const char *ip = "127.0.0.1";
16     in_port_t port = 7070;
17 
18     int i = 0;
19     sockfd = socket(AF_INET, SOCK_STREAM, 0);
20     struct sockaddr_in cliaddr;
21     bzero(&cliaddr, sizeof(cliaddr));
22     cliaddr.sin_family = AF_INET;
23     inet_aton(ip, &cliaddr.sin_addr);
24     cliaddr.sin_port = htons(port);
25 
26     int ret = connect(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
27     str_cli(stdin, sockfd);
28 
29     return 0;
30 }
31 
32 void str_cli(FILE *fp, int sockfd) {
33     char sendline[MAXLINE], recvline[MAXLINE];
34 
35     while (fgets(sendline, MAXLINE, fp) != NULL) {
36         write(sockfd, sendline, strlen(sendline));
37 
38         if (read(sockfd, recvline, MAXLINE) == 0) {
39             printf("server terminated prematurely\n");
40         }
41         fputs(recvline, stdout);
42         bzero(recvline, MAXLINE);
43     }
44 }

編譯:gcc client.c -o client

客戶端的流程:

一、創建套接字

二、發起tcp鏈接

#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);

connect也是阻塞函數,tcp鏈接成功後返回0。

 

到這裏咱們完成了一個超級簡單的服務器-客戶端程序的開發。以後咱們會對這個程序不斷完善。

 

下文:

本篇中寫的服務器,fork的子進程執行完直接調exit了,咱們知道子進程結束後可是父進程沒有回收其對應的空間(進程號等),隨着子進程的不停申請,但得不到釋放,內核會內存泄露,也就是變成了殭屍進程。下一篇,咱們引入對子進程的空間釋放解決這個問題。

相關文章
相關標籤/搜索