手機上的APP是如何與服務器通訊的

絮叨
數據庫


       講解CS通訊以前,先大體瞭解一下咱們平時手機通話的流程。語音信號通過脈衝採樣變成數字信號,經過手機GSM模塊發送無線信號至基站進入無線接入網,根據對方手機號查詢數據庫後經過骨幹路由器轉入核心網,一連串中轉以後發送到對端所屬的小區,找一條空閒線路接通對方。服務器

       網絡通訊相似,可是也有不一樣,電話信號只能維持一條鏈接,而一個服務端能夠維持多條鏈接,像雙十一淘寶OceanBase就達到了一千萬QPS的併發量。微信

這裏實名給手淘打個招聘廣告網絡

基礎知識併發

瞭解APP通訊首先要了解socket的含義。Socket是一種進程通訊方式,可用於多主機之間的通訊,IP地址(對應主機)和端口(對應進程)就肯定了一個socket,相似於電話的插座。下面咱們來實現一個基礎網絡示例:客戶端從標準輸入讀取文本,發送給服務器;服務器接收後原文返回給客戶端,客戶端輸出到標準輸出。socket


注:標準輸入STDIN位於 /dev/stdin ,通常爲鍵盤輸入,fd爲0;標準輸出STDOUT位於/dev/stdout,通常爲終端顯示器,fd爲1;標準錯誤 STDERR位於/dev/stderr,fd爲2。tcp

       TCP客戶/服務端程序基本流程以下:函數



服務端處理流程ui

服務端程序以下:url


#include<sys/socket.h> /* basic socket definitions */int main(int argc, char **argv){ int listenfd,connfd; pid_t childpid; socklen_t clilen; struct sockaddr_incliaddr, 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, LISTENQ);      //監聽
for ( ; ; ) { clilen = sizeof(cliaddr); connfd = Accept(listenfd, (structsockaddr *) &cliaddr, &clilen); //阻塞等待客戶端SYN報文
if ( (childpid = Fork()) == 0) { /* fork一個子進程專門處理接入的客戶端 */ Close(listenfd); /* 子進程關閉監聽端口 */ str_echo(connfd); /* 子進程發送請求 */ exit(0); } Close(connfd); /* 父進程關閉鏈接端口 */ }}


下面分析一下服務端狀態機流程:

服務端建立一個監聽套接字並綁定本機知名端口(如80、8080http端口),本機地址設置爲INADDR_ANY是爲了任何本地接口的鏈接都接收,通常爲多網卡的場景。以後服務端阻塞在accpt調用,使用fork爲每一個客戶端專門分配一個子進程,父進程繼續監聽接入的客戶端。

對於已鏈接的客戶端,使用str_echo讀入客戶端發送過來的數據,並直接返回回去。


void str_echo(intsockfd){ ssize_t n; char buf[MAXLINE];
again: while ( (n = read(sockfd, buf, MAXLINE))> 0) //從標準輸入讀取數據 Writen(sockfd, buf, n); //發送至服務端 // while循環退出說明接收到FIN包,客戶端完成了數據發送 if (n < 0 && errno == EINTR) goto again; //被信號打斷,繼續讀取 else if (n < 0) err_sys("str_echo: readerror"); //遇到其餘錯誤結束運行}


客戶端處理流程

下面給出客戶端處理狀態機(省略部分socket異常處理):


#include<sys/socket.h>int main(int argc, char **argv){ int i,sockfd[5]; struct sockaddr_inservaddr;
if (argc != 2) err_quit("usage: tcpcli <IPaddress>");
for (i = 0; i < 5; i++) { sockfd[i] = Socket(AF_INET,SOCK_STREAM, 0); //建立套接字
bzero(&servaddr,sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port =htons(SERV_PORT); inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
Connect(sockfd[i], (SA *)&servaddr, sizeof(servaddr)); //鏈接服務器 }
str_cli(stdin, sockfd[0]); /* 客戶端數據讀寫操做 */
exit(0);}


str_cli處理邏輯以下:


void str_cli(FILE* fp, int sockfd){ charsendline[MAXLINE],recvline[MAXLINE];
while (fgets(sendline, MAXLINE, fp) !=NULL) { //從fp讀入數據
Writen(sockfd, sendline,strlen(sendline)); //發送給服務器
if (Readline(sockfd, recvline,MAXLINE) == 0) //接收服務器發送過來的數據 err_quit("str_cli:server terminated prematurely"); //若是爲0,說明服務端已關閉鏈接
fputs(recvline, stdout); //將接收到的數據輸出到終端 } //文件讀取結束時fgets返回NULL,while退出}


運行客戶端/服務端程序

服務器啓動後,在客戶端鏈接以前,使用netstat -a檢查主機監聽套接字狀態以下:


Proto Local Address Foreign Address StateTCP *:9877 *:* LISTEN


來啓動客戶端並指定服務器地址127.0.0.1(本地環回地址),客戶端在connect函數中完成TCP三次握手流程,以後服務端從accept中返回,一條數據通道創建。

服務端這邊握手流程較爲複雜,用簡圖表示以下:



鏈接創建後,客戶端阻塞於fgets等待接收鍵盤輸入,服務端進程從accept返回後調用fork建立一個子進程專門負責這條鏈接,父進程繼續阻塞在accept上監聽新客戶端的到來。此時,三個進程都阻塞:客戶端進程、服務器父進程、服務器子進程。

注1:一個程序不等於一個進程,像淘寶,除了主進程進行各類數據處理外,還有push進程做爲維持客戶端和服務器的長鏈接通訊,用於發送心跳包和推送消息。

注2:創建鏈接時,客戶端阻塞在connect上,收到服務器的SYN/ACK報文即返回,而服務器須要收到ACK報文才返回,兩邊阻塞時間差了半個RTT。

使用netstat -a觀察如今鏈接狀況:

Proto Local Address Foreign Address StateTCP localhost:9877 localhost:47512 ESTABLISHED //服務器TCP localhost:47512 localhost:9877 ESTABLISHED //客戶端TCP *:9877 *:* LISTEN //服務器父進程


       能夠看到雙方socket已處於ESTABLISHED狀態,接下來客戶端能夠和服務器進行數據收發。當客戶端輸入EOF字符(按下Control+Z表示終止輸入)時,fgets返回空指針,客戶端數據處理函數str_cli返回,客戶端main函數調用exit終止進程。進程終止會關閉全部打開的文件描述符,所以客戶端會發送FIN報文給服務器,服務器子進程迴應ACK後也調用exit函數關閉文件描述符,發送FIN報文。

這裏除了經過TCP四次揮手正常終止鏈接,還能夠發送信號kill -9 pid終止進程。信號的處理後續剖析~

上述程序對服務器主機崩潰、主機重啓、主機關機及客戶端主機崩潰等異常狀況都作了保護,這也是咱們平時寫須要注意程序健壯性的地方。


本文分享自微信公衆號 - 機械猿(on_ourway)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索