目錄
客戶/服務器程序源碼
POSIX信號處理
POSIX信號語義
處理SIGCHLD信號
處理僵死進程
處理被中斷的系統(tǒng)調(diào)用
wait和waitpid函數(shù)
wait和waitpid函數(shù)的區(qū)別
網(wǎng)絡編程可能會遇到的三種情況
TCP程序小結
數(shù)據(jù)格式
回射輸入行這樣一個客戶/服務器程序是一個雖然簡單然而卻很有效的網(wǎng)絡應用程序的例子。實現(xiàn)任何客戶/服務器網(wǎng)絡應用所需的所有基本步驟可通過本例子闡明。若想把本例子擴充成你自己的應用程序,你只需修改服務器對于來自客戶的輸入的處理過程。
/* tcpserv01.c */#include <sys/socket.h>#include <strings.h>#include <sys/types.h>#include <netinet/in.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <errno.h>intmain(int argc, char **argv){ int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); exit(1); } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(9877); if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("bind"); exit(1); } if(listen(listenfd, 5) < 0) { perror("listen"); exit(1); } for(;;) { clilen = sizeof(cliaddr); if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) { perror("accept"); exit(1); } if((childpid = fork()) < 0) { perror("fork"); exit(1); } else if(childpid == 0) /* child PRocess */ { if(close(listenfd) < 0) /* close listening socket */ { perror("child close"); exit(1); } str_echo(connfd); /* process the request */ exit(0); } if(close(connfd) < 0) /* parent close connected socket */ { perror("parent close"); exit(1); } } }TCP回射服務器程序:str_echo函數(shù)
/* str_echo.c */#include <stdio.h>#include <stdlib.h>#include <errno.h>voidstr_echo(int sockfd){ ssize_t n; char buf[4096];again: while((n = read(sockfd, buf, 4096)) > 0) writen(sockfd, buf, n); if(n < 0 && errno == EINTR) goto again; else if(n < 0) { perror("read"); exit(1); } }TCP回射客戶程序:main函數(shù)
/* tcpcli01.c */#include <stdio.h>#include <strings.h>#include <string.h>#include <stdlib.h>#include <errno.h>#include <unistd.h>#include <sys/types.h>#include <netinet/in.h>#include <sys/socket.h>intmain(int argc, char **argv){ int sockfd; struct sockaddr_in servaddr; if(argc != 2) { printf("usage: tcpcli <ipaddress> "); exit(0); } if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); exit(1); } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(9877); if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) < 0) { perror("inet_pton"); exit(1); } if(connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("connect"); exit(1); } str_cli(stdin, sockfd); /* do it all */ exit(0);}TCP回射客戶程序:str_cli函數(shù)
/* str_cli.c */#include <stdio.h>#include <stdlib.h>#include <string.h>voidstr_cli(FILE *fp, int sockfd){ char sendline[4096], recvline[4096]; while(fgets(sendline, 4096, fp) != NULL) { writen(sockfd, sendline, strlen(sendline)); if(readline(sockfd, recvline, 4096) == 0) { printf("str_cli: server terminated prematurely"); exit(0); } fputs(recvline, stdout); }}服務器和客戶都要調(diào)用的自定義函數(shù):
#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <unistd.h>#include <sys/types.h>ssize_t /* read "n" bytes from a descriptor. */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; /* and call read() again */ else return(-1); } else if(nread == 0) break; /* EOF */ nleft -= nread; ptr += nread; } return(n - nleft); /* return >= 0 */}ssize_t /* write n bytes to a descriptor */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; /* and call write again */ else return(-1); /* error */ } nleft -= nwritten; ptr += nwritten; } return(n - nwritten);}ssize_treadline(int fd, void *vptr, size_t maxlen){ ssize_t n, rc; char c, *ptr; ptr = vptr; for(n = 1; n < maxlen; n++) { again: if((rc = read(fd, &c, 1)) == 1) { *ptr++ = c; if(c == '/n') break; /* newline is stored, like fgets() */ } else if(rc == 0) { *ptr = 0; return(n - 1); /* EOF, n - 1 bytes were read */ } else { if(errno == EINTR) goto again; return(-1); /* error, errno set by read() */ } } *ptr = 0; /* null terminate like fgets() */ return(n);}正常啟動:
首先,我們在主機linux上后臺啟動服務器。
服務器啟動后,它調(diào)用socket、bind、listen和accept,并阻塞于accept調(diào)用。(我們還沒有啟動客戶。)在啟動客戶之前,我們運行netstat程序來檢查服務器監(jiān)聽套接口的狀態(tài)。netstat -a
這個輸出正是我們所期望的:套接口處于LISTEN狀態(tài),它有通配的本地IP地址,本地端口號為9877(這正是我們所配置的端口號)。netstat用星號“*”來表示一個為0的IP地址(INADDR_ANY,通配地址)或為0的端口號。
我們接著在同一個主機上啟動客戶,并指定服務器主機的IP地址為127.0.0.1(回饋地址)。當然我們也可以指定該地址為 該主機的普通(非回饋)IP地址。
客戶調(diào)用socket和connect,后者引起TCP的三路握手過程。當三路握手完成后,客戶中的connect和服務器中的accept均返回,連接于是建立。
服務器父進程再次調(diào)用accept并阻塞,等待下一個客戶連接。
我們特意在同一個主機上運行客戶和服務器,因為這是試驗客戶/服務器應用程序的最簡單方法。既然我們是在同一個主機上運行客戶和服務器,netstat對于所建立的TCP連接給出兩行輸出(下圖紅色框內(nèi))。
第一個ESTABLISHED行對應于服務器子進程的套接口,因為它的本地端口號是9877;第二個ESTABLISHED行對應于客戶進程的套接口,因為它的本地端口號是54076. 要是我們在不同的主機上運行客戶和服務器,那么客戶主機就只輸出客戶進程的套接口,服務器主機也只輸出兩個服務器進程(一個父進程、一個子進程)的套接口。
正常終止:至此連接已經(jīng)建立,我們在客戶的標準輸入中不論鍵入什么,都回射到它的標準輸出中。
注:<Ctrl-D>是我們的終端EOF字符
此時如果立即執(zhí)行netstat命令,我們將看到如下結果:
當前連接的客戶端(它的本地端口號為60779)進入了TIME_WAIT狀態(tài),而監(jiān)聽服務器仍在等待另一個客戶連接。
在服務器子進程終止時,給父進程發(fā)送一個SIGCHILD信號。這一點在本例中發(fā)生了,但是我們沒有在代碼中捕獲該信號,而 該信號的缺省行為是被忽略。既然父進程未加處理,子進程于是進入僵死狀態(tài)(http://www.CUOXin.com/nufangrensheng/p/3509618.html)。我們可以使用ps命令驗證這一點。
子進程的狀態(tài)現(xiàn)在是Z(表示僵死)。父進程的狀態(tài)現(xiàn)在是S(表示為等待某種資源而睡眠)。
我們必須清理僵死進程,這就涉及到UNIX信號的處理。
POSIX信號處理信號(signal)就是通知某個進程發(fā)生了某個事件,有時也稱為軟件中斷(software interrupt)。信號通常是異步發(fā)生的,也就是說進程預先不知道信號準確發(fā)生的時刻。
信號可以:
(1)由一個進程發(fā)給另一個進程(或自身)。
(2)由內(nèi)核發(fā)給某個進程。
SIGCHILD信號就是由內(nèi)核在任何一個進程終止時發(fā)給它的父進程的一個信號。
每個信號都有一個與之關聯(lián)的處置(disposition),也稱為行為(action)。我們調(diào)用sigaction函數(shù)(http://www.CUOXin.com/nufangrensheng/p/3515945.html)或signal函數(shù)(http://www.CUOXin.com/nufangrensheng/p/3514547.html)來設定一個信號的處置,并有三種選擇。
(1)我們可以提供一個函數(shù),它將在特定信號發(fā)生的任何時刻被調(diào)用。這樣的函數(shù)稱為信號處理函數(shù)(signal handler),這種行為稱為捕獲(catching)信號。有兩個信號不能被捕獲,它們是SIGKILL和SIGSTOP。信號處理函數(shù)由信號值這個單一的整數(shù)參數(shù)來調(diào)用,且沒有返回值,其函數(shù)原型如下:
void handler( int signo );
對于大多數(shù)信號來說,調(diào)用sigaction函數(shù)并指定信號發(fā)生時所調(diào)用的函數(shù)就是捕獲信號所要做的全部工作。不過,SIGIO、SIGPOLL和SIGURG這些個別信號還要求捕獲它們的進程做些額外工作。
(2)我們可以把某個信號的處置設定為SIG_IGN來忽略(ignore)它。SIGKILL和SIGSTOP這兩個信號不能被忽略。
(3)我們可以把某個信號的處置設定為SIG_DFL來啟用它的缺省(default)處置。缺省處置通常是在收到信號后終止進程,其中某些信號還在當前工作目錄產(chǎn)生一個進程的核心映像(core image,也稱為內(nèi)存映像)。另有個別的缺省處理是忽略:SIGCHLD和SIGURG(帶外數(shù)據(jù)到達時發(fā)送)就是缺省處置為忽略的兩個信號。
POSIX信號語義我們把符合POSIX的系統(tǒng)上的信號處理總結如下:
(1)一旦安裝了信號處理函數(shù),它便一直安裝著(較早期的系統(tǒng)是每執(zhí)行一次就將其拆除)。
(2)在一個信號處理函數(shù)運行期間,正被遞交的信號是阻塞的。而且,安裝處理函數(shù)時在傳遞給sigaction函數(shù)的sa_mask信號集中指定的任何額外信號也被阻塞。
(3)如果一個信號在被阻塞期間產(chǎn)生了一次或多次,那么該信號被解阻塞之后通常只遞交一次,也就是說UNIX信號缺省是不排隊的。
(4)利用siagprocmask函數(shù)(http://www.CUOXin.com/nufangrensheng/p/3515257.html)選擇性地阻塞或解阻塞一組信號是可能的。這使得我們可以做到在一段臨界區(qū)代碼執(zhí)行期間,防止捕獲某些信號,以此保護這段代碼。
處理SIGCHILD信號設置僵死(zombie)狀態(tài)的目的是維護子進程的信息,以便父進程在以后某個時候獲取。這些信息包括子進程的進程ID、終止狀態(tài)以及資源利用信息(CPU時間、內(nèi)存使用量等等)。如果一個進程終止,而該進程有子進程處于僵死狀態(tài),那么它的所有僵死子進程的父進程ID將被重置為1(init進程)。繼承這些子進程的init進程將清理它們(也就是說init進程將wait它們,從而去除它們的僵死狀態(tài))。有些UNIX系統(tǒng)在ps命令輸出的COMMAND欄以<defunct>指明僵死進程。
處理僵死進程我們顯然不愿意留存僵死進程。它們占用內(nèi)核中的空間,最終可能導致我們耗盡進程資源。無論何時我們fork子進程都得wait它們,以防它們變成僵死進程。為此我們建立一個俘獲SIGCHLD信號的信號處理函數(shù),在函數(shù)體中我們調(diào)用wait。通過在TCP回射服務器程序:main函數(shù)中的listen調(diào)用之后增加如下函數(shù)調(diào)用:
signal(SIGCHLD, sig_chld);
這樣我們就建立了該信號處理函數(shù)。(這必須在fork第一個子進程之前完成,并且只做一次。) 我們接著定義名為sig_chld的這個信號處理函數(shù),如下:
void sig_chld(int signo){ pid_t pid; int stat; pid = wait(&stat); printf("child %d terminatted/n", pid); return;}
處理僵死進程的可移植方法就是捕獲SIGCHLD,并調(diào)用wait或waitpid。
新的問題是:在某些系統(tǒng)上(這些 系統(tǒng)標準C函數(shù)庫中提供的signal函數(shù)不會致使內(nèi)核自動重啟被中斷的系統(tǒng)調(diào)用),SIGCHLD信號被捕獲并處理后,慢系統(tǒng)調(diào)用accept會返回一個EINTR錯誤(被中斷的系統(tǒng)調(diào)用).
處理被中斷的系統(tǒng)調(diào)用慢系統(tǒng)調(diào)用(slow system call)是指那些可能永遠阻塞的系統(tǒng)調(diào)用(調(diào)用有可能永遠無法返回)。多數(shù)網(wǎng)絡支持函數(shù)都屬于這一類。
適用于慢系統(tǒng)調(diào)用的基本規(guī)則是:當阻塞于某個慢系統(tǒng)調(diào)用的一個進程捕獲某個信號且相應信號處理函數(shù)返回時,該系統(tǒng)調(diào)用可能返回一個EINTR錯誤。有些內(nèi)核自動重啟某些被中斷的系統(tǒng)調(diào)用。
為了處理被中斷的accept,我們把對accept的調(diào)用從for循環(huán)開始修改如下:
for(;;){ clilen = sizeof(cliaddr); if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) { if(errno == EINTR) continue; /* back to for() */ else { perror("accept error"); exit(1); } }}
這段代碼所做的事情就是自己重啟被中斷的系統(tǒng)調(diào)用。對于accept以及諸如read、write、select和open之類函數(shù)來說,這是合適的。不過有一個函數(shù)我們不能重啟:connect。如果該函數(shù)返回EINTR,我們就不能再次調(diào)用它,否則將立即返回一個錯誤。當connect被一個捕獲的信號中斷而且不自動重啟時,我們必須調(diào)用select來等待連接完成。
wait和waitpid函數(shù)#include <sys/wait.h>pid_t wait(int *statloc);pid_t waitpid(pid_t pid, int *statloc, int options);二者均返回:若成功則返回已終止子進程的進程ID,若出錯則返回-1
函數(shù)wait和waitpid均返回兩個值:函數(shù)返回值是已終止子進程ID號,子進程的終止狀態(tài)(一個整數(shù))則通過statloc指針返回。我們可以調(diào)用三個宏來檢查終止狀態(tài),并辨別子進程是正常終止、由某個信號殺死還是僅僅由作業(yè)控制停止而已(http://www.CUOXin.com/nufangrensheng/p/3510101.html)。
如果調(diào)用wait的進程沒有已終止的子進程,不過有一個或多個子進程仍在執(zhí)行,那么wait將阻塞到現(xiàn)有子進程第一個終止為止。
waitpid函數(shù)對于等待哪個進程以及是否阻塞給了我們更多的控制。首先,pid參數(shù)允許我們指定想等待的進程ID,值-1表示等待第一個終止的子進程。其次,options參數(shù)允許我們指定附加選項。最常用的選項是WNOHANG,它告知內(nèi)核在沒有已終止子進程時不要阻塞。
函數(shù)wait和waipid的區(qū)別為了說明wait和waitpid的區(qū)別,我們試想如下情況:
當客戶終止時,所有打開的描述符字由內(nèi)核自動關閉,且所有5個連接基本在同一時刻終止。這就引發(fā)了5個FIN,每個連接一個,它們反過來使服務器的5個子進程基本在同一時刻終止。這又導致差不多在同一時刻遞交5個SIGCHLD信號給父進程。
如果我們調(diào)用函數(shù)wait來處理已終止的子進程,那么只會捕獲到一個SIGCHLD信號。也就是說,其他的4個子進程仍然作為僵死進程存在著。
所以,建立一個信號處理函數(shù)并在其中調(diào)用wait并不足以防止出現(xiàn)僵死進程。本問題在于:所有5個信號都在信號處理函數(shù)執(zhí)行之前產(chǎn)生,而信號處理函數(shù)只執(zhí)行一次,因為UNIX信號一般是不排隊的。
正確的解決辦法是調(diào)用waitpid而不是wait。如下所示給出了正確處理SIGCHLD的sig_chld函數(shù)的版本。這個版本管用的原因在于:我們在一個循環(huán)內(nèi)調(diào)用waitpid,以獲取所有已終止子進程的狀態(tài)。我們必須指定WNOHANG選項,它告知waitpid在有尚未終止的子進程在運行時不要阻塞。我們不能在循環(huán)內(nèi)調(diào)用wait,因為沒有辦法防止wait在尚有未終止的子進程在運行時阻塞。
voidsig_chld(int signo){ pid_t pid; int stat; while((pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("child %d terminated/n", pid); return;}
服務器的最終版本
/* tcpserv01.c */#include <sys/socket.h>#include <strings.h>#include <sys/types.h>#include <netinet/in.h>#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <errno.h>intmain(int argc, char **argv){ int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr, servaddr; void sig_chld(int); if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); exit(1); } bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(9877); if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("bind"); exit(1); } if(listen(listenfd, 5) < 0) { perror("listen"); exit(1); } signal(SIGCHLD, sig_chld); /* must call waitpid() */ for(;;) { clilen = sizeof(cliaddr); if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0) { if(errno == EINTR) continue; /* back to for */ else { perror("accept"); exit(1); } } if((childpid = fork()) < 0) { perror("fork"); exit(1); } else if(childpid == 0) /* child process */ { if(close(listenfd) < 0) /* close listening socket */ { perror("child close"); exit(1); } str_echo(connfd); /* process the request */ exit(0); } if(close(connfd) < 0) /* parent close connected socket */ { perror("parent close"); exit(1); } } }在網(wǎng)絡編程時可能會遇到的三種情況:
(1)當fork子進程時,必須捕獲SIGCHLD信號。
(2)當捕獲信號時,必須處理被中斷的系統(tǒng)調(diào)用。
(3)SIGCHLD的信號處理函數(shù)必須正確編寫,應使用waitpid函數(shù)以免留下僵死進程。
TCP程序實例小結:在客戶和服務器可以彼此通信之前,每一端都得指定連接的套接口對:本地IP地址、本地端口、遠地IP地址、遠地端口。在下圖中我們以粗體圓點標出了這四個值。該圖處于客戶端的角度。遠地IP地址和遠地端口必須在客戶端調(diào)用connect時指定,而兩個本地值通常就由內(nèi)核作為connect的一部分來選定??蛻粢部稍谡{(diào)用connect之前,通過調(diào)用bind來指定其中一個或全部兩個本地值,不過這么做不常見。
客戶可以在連接建立后通過調(diào)用getsockname獲取由內(nèi)核指定的兩個本地值。
下圖中標出了同樣的四個值,不過是處于服務器的角度。
本地端口(服務器眾所周知的端口)由bind指定。bind調(diào)用中指定的本地IP地址通常是通配IP地址,盡管服務器也可以指定一個非通配的IP地址來限定接收目標為某個特定本地接口的連接。如果服務器在一個多宿主機上綁定通配IP地址,那么它可以在連接建立后通過調(diào)用getsockname來確定本地IP地址。兩個遠地值則由accept調(diào)用返回給服務器。如果另外一個程序由調(diào)用accept的服務器通過調(diào)用exec來執(zhí)行,則這個新程序可以在必要時調(diào)用getpeername來確定客戶的IP地址和端口號。
例子:在客戶和服務器之間 傳遞文本串
修改我們的服務器程序,它仍然從客戶讀入一行文本,不過新的服務器期望該文本行包含由空格分開的兩個整數(shù),服務器將返回這兩個整數(shù)的和。我們的客戶和服務器程序的main函數(shù)仍保持不變,str_cli函數(shù)也保持不變,所有修改都在str_echo函數(shù)中,如下所示:
#include <stdio.h>#include <sys/types.h>#include <stdlib.h>#include <unistd.h>#include <string.h>voidstr_echo(int sockfd){ long arg1, arg2; ssize_t n; char line[4096]; for(;;) { if((n = readline(sockfd, line, 4096)) == 0) return; /* connection closed by other end */ if(sscanf(line, "%ld%ld", &arg1, &arg2) == 2) snprintf(line, sizeof(line), "%ld/n", arg1 + arg2); else snprintf(line, sizeof(line), "input error/n"); n = strlen(line); writen(sockfd, line, n); }}
我們調(diào)用sscanf把文本串中的兩個參數(shù)轉換為長整數(shù),然后調(diào)用snprintf把結果轉換為文本串。
不論客戶和服務器主機的字節(jié)序如何,這個新的客戶和服務器對都工作的很好。
例子:在客戶與服務器之間傳遞二進制結構
現(xiàn)在我們 把客戶和服務器程序修改為穿越套接口傳遞二進制值而不是文本串。
我們的客戶和服務器程序的main函數(shù)無需改動。另外我們給兩個參數(shù)定義了一個結構,給結果定義了另一個結構。
#ifndef _COMMON_H#define _COMMON_Hstruct args{ long arg1; long arg2; };struct result{ long sum;};#endif
/* str_cli09.c */#include "common.h"#include <stdio.h>#include <stdlib.h>#include <string.h>voidstr_cli(FILE *fp, int sockfd){ char sendline[4096], recvline[4096]; struct args args; struct result result; while(fgets(sendline, 4096, fp) != NULL) { if(sscanf(sendline, "%ld%ld", &args.arg1, &args.arg2) != 2) { printf("invalid input: %s", sendline); continue; } writen(sockfd, &args, sizeof(args)); if(readn(sockfd, &result, sizeof(result)) == 0) { printf("str_cli: server terminated prematurely"); exit(1); } printf("%ld/n", result.sum); }}
/* str_echo09.c */#include <stdio.h>#include "common.h"#include <stdlib.h>#include <errno.h>voidstr_echo(int sockfd){ ssize_t n; struct args args; struct result result; for(;;) { if((n = readn(sockfd, &args, sizeof(args))) == 0) return; result.sum = args.arg1 + args.arg2; writen(sockfd, &result, sizeof(result)); } }
如果我們在具有相同體系結構的兩個主機上運行我們的客戶和服務器程序,那么什么問題都沒有。但是如果在具有不同體系結構的兩個主機上運行同樣的客戶和服務器程序(例如服務器運行在大端系統(tǒng),而客戶運行在小端系統(tǒng)上),那就無法工作了。
本例子實際上存在三個潛在的問題:
(1)不同的實現(xiàn)以不同的格式存儲二進制數(shù)。(大端和小端)
(2)不同的實現(xiàn)在存儲相同的C數(shù)據(jù)類型上可能存在差異。(32位系統(tǒng)和64位系統(tǒng))
(3)不同的實現(xiàn)給結構打包的方式存在差異,這取決于各種數(shù)據(jù)類型所用的位數(shù)以及機器的對齊限制。
因此,穿越套接口傳送二進制結構絕不是明智的選擇。
解決這種數(shù)據(jù)格式問題有兩個常用的方法:
(1)把所有的數(shù)值數(shù)據(jù)作為文本串來傳遞。當然這里假設客戶和服務器主機具有相同的字符集。
(2)顯示定義所支持數(shù)據(jù)類型的二進制格式(位數(shù)、大端或小端),并以這樣的格式在客戶和服務器之間傳遞所有數(shù)據(jù)。
新聞熱點
疑難解答