我的認爲《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了,咱們知道子進程結束後可是父進程沒有回收其對應的空間(進程號等),隨着子進程的不停申請,但得不到釋放,內核會內存泄露,也就是變成了殭屍進程。下一篇,咱們引入對子進程的空間釋放解決這個問題。