網絡中進程之間如何通信
為了方便大家獲取源代碼,可以移步這里,?GitHub源代碼
進程通信的概念最初來源于單機系統(tǒng)。由于每個進程都在自己的地址范圍內運行,為保證兩個相互通信的進程之間既互不干擾又協調一致工作,操作系統(tǒng)為進程通信提供了相應設施,如UNIX BSD有:管道(pipe)、命名管道(named pipe)軟中斷信號(signal)UNIX system V有:消息(message)、共享存儲區(qū)(shared memory)和信號量(semaphore)等。
他們都僅限于用在本機進程之間通信。網間進程通信要解決的是不同主機進程間的相互通信問題(可把同機進程通信看作是其中的特例)。為此,首先要解決的是網間進程標識問題。同一主機上,不同進程可用進程號(process ID)唯一標識。但在網絡環(huán)境下,各主機獨立分配的進程號不能唯一標識該進程。例如,主機A賦于某進程號5,在B機中也可以存在5號進程,因此,“5號進程”這句話就沒有意義了。 其次,操作系統(tǒng)支持的網絡協議眾多,不同協議的工作方式不同,地址格式也不同。因此,網間進程通信還要解決多重協議的識別問題。
其實TCP/IP協議族已經幫我們解決了這個問題,網絡層的“ip地址”可以唯一標識網絡中的主機,而傳輸層的“協議+端口”可以唯一標識主機中的應用程序(進程)。這樣利用三元組(ip地址,協議,端口)就可以標識網絡的進程了,網絡中的進程通信就可以利用這個標志與其它進程進行交互。
使用TCP/IP協議的應用程序通常采用應用編程接口:UNIX? BSD的套接字(socket)和UNIX System V的TLI(已經被淘汰),來實現網絡進程之間的通信。就目前而言,幾乎所有的應用程序都是采用socket,而現在又是網絡時代,網絡中進程通信是無處不在,這就是我為什么說“一切皆socket”。
什么是TCP/IP、UDP
TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,是一個工業(yè)標準的協議集,它是為廣域網(WANs)設計的。
TCP/IP協議存在于OS中,網絡服務通過OS提供,在OS中增加支持TCP/IP的系統(tǒng)調用——Berkeley套接字,如Socket,Connect,Send,Recv等
UDP(User Data Protocol,用戶數據報協議)是與TCP相對應的協議。它是屬于TCP/IP協議族中的一種。如圖:


Socket是什么
1、 socket套接字:
socket起源于Unix,而Unix/Linux基本哲學之一就是“一切皆文件”,都可以用“打開open –> 讀寫write/read –> 關閉close”模式來操作。Socket就是該模式的一個實現,? ? ? ? socket即是一種特殊的文件,一些socket函數就是對其進行的操作(讀/寫IO、打開、關閉).? ? 說白了Socket是應用層與TCP/IP協議族通信的中間軟件抽象層,它是一組接口。在設計模式中,Socket其實就是一個門面模式,它把復雜的TCP/IP協議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數據,以符合指定的協議。
注意:其實socket也沒有層的概念,它只是一個facade設計模式的應用,讓編程變的更簡單。是一個軟件抽象層。在網絡編程中,我們大量用的都是通過socket實現的。
2、套接字描述符
其實就是一個整數,我們最熟悉的句柄是0、1、2三個,0是標準輸入,1是標準輸出,2是標準錯誤輸出。0、1、2是整數表示的,對應的FILE *結構的表示就是stdin、stdout、stderr
套接字API最初是作為UNIX操作系統(tǒng)的一部分而開發(fā)的,所以套接字API與系統(tǒng)的其他I/O設備集成在一起。特別是,當應用程序要為因特網通信而創(chuàng)建一個套接字(socket)時,操作系統(tǒng)就返回一個小整數作為描述符(descriptor)來標識這個套接字。然后,應用程序以該描述符作為傳遞參數,通過調用函數來完成某種操作(例如通過網絡傳送數據或接收輸入的數據)。
在許多操作系統(tǒng)中,套接字描述符和其他I/O描述符是集成在一起的,所以應用程序可以對文件進行套接字I/O或I/O讀/寫操作。
當應用程序要創(chuàng)建一個套接字時,操作系統(tǒng)就返回一個小整數作為描述符,應用程序則使用這個描述符來引用該套接字需要I/O請求的應用程序請求操作系統(tǒng)打開一個文件。操作系統(tǒng)就創(chuàng)建一個文件描述符提供給應用程序訪問文件。從應用程序的角度看,文件描述符是一個整數,應用程序可以用它來讀寫文件。下圖顯示,操作系統(tǒng)如何把文件描述符實現為一個指針數組,這些指針指向內部數據結構。
對于每個程序系統(tǒng)都有一張單獨的表。精確地講,系統(tǒng)為每個運行的進程維護一張單獨的文件描述符表。當進程打開一個文件時,系統(tǒng)把一個指向此文件內部數據結構的指針寫入文件描述符表,并把該表的索引值返回給調用者 。應用程序只需記住這個描述符,并在以后操作該文件時使用它。操作系統(tǒng)把該描述符作為索引訪問進程描述符表,通過指針找到保存該文件所有的信息的數據結構。
**? 針對套接字的系統(tǒng)數據結構:**
1)、套接字API里有個函數socket,它就是用來創(chuàng)建一個套接字。套接字設計的總體思路是,單個系統(tǒng)調用就可以創(chuàng)建任何套接字,因為套接字是相當籠統(tǒng)的。一旦套接字創(chuàng)建后,應用程序還需要調用其他函數來指定具體細節(jié)。例如調用socket將創(chuàng)建一個新的描述符條目:
2)、雖然套接字的內部數據結構包含很多字段,但是系統(tǒng)創(chuàng)建套接字后,大多數字字段沒有填寫。應用程序創(chuàng)建套接字后在該套接字可以使用之前,必須調用其他的過程來填充這些字段。
3、文件描述符和文件指針的區(qū)別:
文件描述符:在linux系統(tǒng)中打開文件就會獲得文件描述符,它是個很小的正整數。每個進程在PCB(Process Control Block)中保存著一份文件描述符表,文件描述符就是這個表的索引,每個表項都有一個指向已打開文件的指針。
文件指針:C語言中使用文件指針做為I/O的句柄。文件指針指向進程用戶區(qū)中的一個被稱為FILE結構的數據結構。FILE結構包括一個緩沖區(qū)和一個文件描述符。而文件描述符是文件描述符表的一個索引,因此從某種意義上說文件指針就是句柄的句柄(在Windows系統(tǒng)上,文件描述符被稱作文件句柄)。
基本的SOCKET接口函數
在生活中,A要電話給B,A撥號,B聽到電話鈴聲后提起電話,這時A和B就建立起了連接,A和B就可以講話了。等交流結束,掛斷電話結束此次交談。? 打電話很簡單解釋了這工作原理:“open—write/read—close”模式。

服務器端先初始化Socket,然后與端口綁定(bind),對端口進行監(jiān)聽(listen),調用accept阻塞,等待客戶端連接。在這時如果有個客戶端初始化一個Socket,然后連接服務器(connect),如果連接成功,這時客戶端與服務器端的連接就建立了??蛻舳税l(fā)送數據請求,服務器端接收請求并處理請求,然后把回應數據發(fā)送給客戶端,客戶端讀取數據,最后關閉連接,一次交互結束。? **這些接口的實現都是內核來完成。具體如何實現,可以看看linux的內核**
4.1、socket()函數
int?socket(int protofamily, int type, int protocol);//返回sockfd
sockfd是描述符。
socket函數對應于普通文件的打開操作。普通文件的打開操作返回一個文件描述字,而socket()用于創(chuàng)建一個socket描述符(socket descriptor),它唯一標識一個socket。這個socket描述字跟文件描述字一樣,后續(xù)的操作都有用到它,把它作為參數,通過它來進行一些讀寫操作。
正如可以給fopen的傳入不同參數值,以打開不同的文件。創(chuàng)建socket的時候,也可以指定不同的參數創(chuàng)建不同的socket描述符,socket函數的三個參數分別為:
protofamily:即協議域,又稱為協議族(family)。常用的協議族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或稱AF_UNIX,Unix域socket)、AF_ROUTE等等。協議族決定了socket的地址類型,在通信中必須采用對應的地址,如AF_INET決定了要用ipv4地址(32位的)與端口號(16位的)的組合、AF_UNIX決定了要用一個絕對路徑名作為地址。
type:指定socket類型。常用的socket類型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等(socket的類型有哪些?)。
protocol:故名思意,就是指定協議。常用的協議有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它們分別對應TCP傳輸協議、UDP傳輸協議、STCP傳輸協議、TIPC傳輸協議(這個協議我將會單獨開篇討論?。?。
注意:并不是上面的type和protocol可以隨意組合的,如SOCK_STREAM不可以跟IPPROTO_UDP組合。當protocol為0時,會自動選擇type類型對應的默認協議。
當我們調用socket創(chuàng)建一個socket時,返回的socket描述字它存在于協議族(address family,AF_XXX)空間中,但沒有一個具體的地址。如果想要給它賦值一個地址,就必須調用bind()函數,否則就當調用connect()、listen()時系統(tǒng)會自動隨機分配一個端口。
4.2、bind()函數
正如上面所說bind()函數把一個地址族中的特定地址賦給socket。例如對應AF_INET、AF_INET6就是把一個ipv4或ipv6地址和端口號組合賦給socket。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
函數的三個參數分別為:
sockfd:即socket描述字,它是通過socket()函數創(chuàng)建了,唯一標識一個socket。bind()函數就是將給這個描述字綁定一個名字。
addr:一個const struct sockaddr *指針,指向要綁定給sockfd的協議地址。這個地址結構根據地址創(chuàng)建socket時的地址協議族的不同而不同,如ipv4對應的是:
structsockaddr_in{sa_family_tsin_family;/* address family: AF_INET */in_port_tsin_port;/* port in network byte order */structin_addrsin_addr;/* internet address */};/* Internet address. */structin_addr{uint32_ts_addr;/* address in network byte order */};ipv6對應的是:structsockaddr_in6{sa_family_tsin6_family;/* AF_INET6 */in_port_tsin6_port;/* port number */uint32_tsin6_flowinfo;/* IPv6 flow information */structin6_addrsin6_addr;/* IPv6 address */uint32_tsin6_scope_id;/* Scope ID (new in 2.4) */};structin6_addr{unsignedchars6_addr[16];/* IPv6 address */};Unix域對應的是:#defineUNIX_PATH_MAX? ? 108structsockaddr_un{sa_family_tsun_family;/* AF_UNIX */charsun_path[UNIX_PATH_MAX];/* pathname */};
addrlen:對應的是地址的長度。
通常服務器在啟動的時候都會綁定一個眾所周知的地址(如ip地址+端口號),用于提供服務,客戶就可以通過它來接連服務器;而客戶端就不用指定,有系統(tǒng)自動分配一個端口號和自身的ip地址組合。這就是為什么通常服務器端在listen之前會調用bind(),而客戶端就不會調用,而是在connect()時由系統(tǒng)隨機生成一個。
網絡字節(jié)序與主機字節(jié)序
主機字節(jié)序就是我們平常說的大端和小端模式:不同的CPU有不同的字節(jié)序類型,這些字節(jié)序是指整數在內存中保存的順序,這個叫做主機序。引用標準的Big-Endian和Little-Endian的定義如下:
a) Little-Endian就是低位字節(jié)排放在內存的低地址端,高位字節(jié)排放在內存的高地址端。
b) Big-Endian就是高位字節(jié)排放在內存的低地址端,低位字節(jié)排放在內存的高地址端。
網絡字節(jié)序:4個字節(jié)的32 bit值以下面的次序傳輸:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。這種傳輸次序稱作大端字節(jié)序。由于TCP/IP首部中所有的二進制整數在網絡中傳輸時都要求以這種次序,因此它又稱作網絡字節(jié)序。字節(jié)序,顧名思義字節(jié)的順序,就是大于一個字節(jié)類型的數據在內存中的存放順序,一個字節(jié)的數據沒有順序的問題了。
所以:在將一個地址綁定到socket的時候,請先將主機字節(jié)序轉換成為網絡字節(jié)序,而不要假定主機字節(jié)序跟網絡字節(jié)序一樣使用的是Big-Endian。由于這個問題曾引發(fā)過血案!公司項目代碼中由于存在這個問題,導致了很多莫名其妙的問題,所以請謹記對主機字節(jié)序不要做任何假定,務必將其轉化為網絡字節(jié)序再賦給socket。
4.3、listen()、connect()函數
如果作為一個服務器,在調用socket()、bind()之后就會調用listen()來監(jiān)聽這個socket,如果客戶端這時調用connect()發(fā)出連接請求,服務器端就會接收到這個請求。
int listen(int sockfd, int backlog);int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
listen函數的第一個參數即為要監(jiān)聽的socket描述字,第二個參數為相應socket可以排隊的最大連接個數。socket()函數創(chuàng)建的socket默認是一個主動類型的,listen函數將socket變?yōu)楸粍宇愋偷?,等待客戶的連接請求。
connect函數的第一個參數即為客戶端的socket描述字,第二參數為服務器的socket地址,第三個參數為socket地址的長度??蛻舳送ㄟ^調用connect函數來建立與TCP服務器的連接。
4.4、accept()函數
TCP服務器端依次調用socket()、bind()、listen()之后,就會監(jiān)聽指定的socket地址了。TCP客戶端依次調用socket()、connect()之后就向TCP服務器發(fā)送了一個連接請求。TCP服務器監(jiān)聽到這個請求之后,就會調用accept()函數取接收請求,這樣連接就建立好了。之后就可以開始網絡I/O操作了,即類同于普通文件的讀寫I/O操作。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回連接connect_fd
參數sockfd
參數sockfd就是上面解釋中的監(jiān)聽套接字,這個套接字用來監(jiān)聽一個端口,當有一個客戶與服務器連接時,它使用這個一個端口號,而此時這個端口號正與這個套接字關聯。當然客戶不知道套接字這些細節(jié),它只知道一個地址和一個端口號。
參數addr
這是一個結果參數,它用來接受一個返回值,這返回值指定客戶端的地址,當然這個地址是通過某個地址結構來描述的,用戶應該知道這一個什么樣的地址結構。如果對客戶的地址不感興趣,那么可以把這個值設置為NULL。
參數len
如同大家所認為的,它也是結果的參數,用來接受上述addr的結構的大小的,它指明addr結構所占有的字節(jié)個數。同樣的,它也可以被設置為NULL。
如果accept成功返回,則服務器與客戶已經正確建立連接了,此時服務器通過accept返回的套接字來完成與客戶的通信。
注意:
accept默認會阻塞進程,直到有一個客戶連接建立后返回,它返回的是一個新可用的套接字,這個套接字是連接套接字。
此時我們需要區(qū)分兩種套接字,
監(jiān)聽套接字: 監(jiān)聽套接字正如accept的參數sockfd,它是監(jiān)聽套接字,在調用listen函數之后,是服務器開始調用socket()函數生成的,稱為監(jiān)聽socket描述字(監(jiān)聽套接字)
連接套接字:一個套接字會從主動連接的套接字變身為一個監(jiān)聽套接字;而accept函數返回的是已連接socket描述字(一個連接套接字),它代表著一個網絡已經存在的點點連接。
一個服務器通常通常僅僅只創(chuàng)建一個監(jiān)聽socket描述字,它在該服務器的生命周期內一直存在。內核為每個由服務器進程接受的客戶連接創(chuàng)建了一個已連接socket描述字,當服務器完成了對某個客戶的服務,相應的已連接socket描述字就被關閉。
自然要問的是:為什么要有兩種套接字?原因很簡單,如果使用一個描述字的話,那么它的功能太多,使得使用很不直觀,同時在內核確實產生了一個這樣的新的描述字。
連接套接字socketfd_new 并沒有占用新的端口與客戶端通信,依然使用的是與監(jiān)聽套接字socketfd一樣的端口號
4.5、read()、write()等函數
萬事具備只欠東風,至此服務器與客戶已經建立好連接了??梢哉{用網絡I/O進行讀寫操作了,即實現了網咯中不同進程之間的通信!網絡I/O操作有下面幾組:
read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()
我推薦使用recvmsg()/sendmsg()函數,這兩個函數是最通用的I/O函數,實際上可以把上面的其它函數都替換成這兩個函數。它們的聲明如下:
#includessize_tread(intfd,void*buf,size_tcount);ssize_twrite(intfd,constvoid*buf,size_tcount);#include#includessize_tsend(intsockfd,constvoid*buf,size_tlen,intflags);ssize_trecv(intsockfd,void*buf,size_tlen,intflags);ssize_tsendto(intsockfd,constvoid*buf,size_tlen,intflags,conststruct sockaddr *dest_addr,socklen_taddrlen);ssize_trecvfrom(intsockfd,void*buf,size_tlen,intflags,? ? ? ? ? ? ? ? ? ? ? ? struct sockaddr *src_addr,socklen_t*addrlen);ssize_tsendmsg(intsockfd,conststruct msghdr *msg,intflags);ssize_trecvmsg(intsockfd, struct msghdr *msg,intflags);
read函數是負責從fd中讀取內容.當讀成功時,read返回實際所讀的字節(jié)數,如果返回的值是0表示已經讀到文件的結束了,小于0表示出現了錯誤。如果錯誤為EINTR說明讀是由中斷引起的,如果是ECONNREST表示網絡連接出了問題。
write函數將buf中的nbytes字節(jié)內容寫入文件描述符fd.成功時返回寫的字節(jié)數。失敗時返回-1,并設置errno變量。 在網絡程序中,當我們向套接字文件描述符寫時有倆種可能。1)write的返回值大于0,表示寫了部分或者是全部的數據。2)返回的值小于0,此時出現了錯誤。我們要根據錯誤類型來處理。如果錯誤為EINTR表示在寫的時候出現了中斷錯誤。如果為EPIPE表示網絡連接出現了問題(對方已經關閉了連接)。
其它的我就不一一介紹這幾對I/O函數了,具體參見man文檔或者baidu、Google,下面的例子中將使用到send/recv。
4.6、close()函數
在服務器與客戶端建立連接之后,會進行一些讀寫操作,完成了讀寫操作就要關閉相應的socket描述字,好比操作完打開的文件要調用fclose關閉打開的文件。
int close(int fd);
close一個TCP socket的缺省行為時把該socket標記為以關閉,然后立即返回到調用進程。該描述字不能再由調用進程使用,也就是說不能再作為read或write的第一個參數。
注意:close操作只是使相應socket描述字的引用計數-1,只有當引用計數為0的時候,才會觸發(fā)TCP客戶端向服務器發(fā)送終止連接請求。
Socket編程實例
服務器端:一直監(jiān)聽本機的8888號端口,如果收到連接請求,將接收請求并接收客戶端發(fā)來的消息,并向客戶端返回消息。
服務器代碼
/*************************************************************************
> File Name: ser.c
> Author: battle
> Mail: batbattle@163.com?
> Created Time: 2018年06月02日 星期六 11時21分02秒 ************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define PORT 8888
#define BACKLOG 10
#define MAXDATASIZE? 2048
int main(int argc, char *argv[])
{
? ? int listenfd;
? ? //創(chuàng)建一個socket描述符,此描述符僅是本主機上的一個普通文件描述符而已
? ? listenfd = socket(AF_INET, SOCK_STREAM, 0);
? ? printf("listenfd=%d\n", listenfd);
? ? //定義一個結構體變量servaddr,用來記錄給定的IP和port信息,為bind函數做準備
? ? struct sockaddr_in serveraddr;
? ? bzero(&serveraddr, sizeof(serveraddr));
? ? serveraddr.sin_family = AF_INET;
? ? serveraddr.sin_port = htons(PORT); //把端口轉化為網絡字節(jié)序,即大端模式
? ? serveraddr.sin_addr.s_addr = INADDR_ANY;
? ? //把“本地含義的描述符”綁定到一個IP和Port上,此時這個socket才具備對外連接的能力
? ? bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
? ? //創(chuàng)建一個監(jiān)聽隊列,用來保存用戶的請求連接信息(ip、port、protocol)
? ? listen(listenfd, BACKLOG);
? ? printf("======bind success,waiting for client's request======\n");
? ? //讓操作系統(tǒng)回填client的連接信息(ip、port、protocol)
? ? struct sockaddr_in peeraddr;
? ? socklen_t peer_len = sizeof(peeraddr);
? ? int connfd;
? ? while(1)
? ? {
? ? ? ? //accept函數從listen函數維護的監(jiān)聽隊列里取一個客戶連接請求處理
? ? ? ? connfd = accept(listenfd, (struct sockaddr*)&peeraddr, &peer_len);
? ? ? ? printf("\n=====================客戶端鏈接成功=====================\n");
? ? ? ? printf("IP = %s:PORT = %d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
? ? ? ? char buf[MAXDATASIZE];
? ? ? ? while(1)
? ? ? ? {
? ? ? ? ? ? memset(buf, '\0', MAXDATASIZE/sizeof? (char));
? ? ? ? ? ? int recv_length = recv(connfd, buf, MAXDATASIZE/sizeof (char), 0);
? ? ? ? ? ? if(recv_length == 0)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? printf("client has closed!\n");
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? ? ? printf("client say: ");
? ? ? ? ? ? fputs(buf, stdout);
? ? ? ? ? ? memset(buf, '\0', MAXDATASIZE/sizeof (char));
? ? ? ? ? ? printf("input: ");
? ? ? ? ? ? fgets(buf, sizeof(buf), stdin);
? ? ? ? ? ? send(connfd, buf, recv_length, 0);
? ? ? ? }
? ? ? ? close(connfd);
? ? ? ? close(listenfd);
? ? ? ? return 0;
}}
客戶端代碼實例
/*************************************************************************
> File Name: cli.c
> Author: battle
> Mail: batbattle@163.com?
> Created Time: 2018年06月02日 星期六 10時55分37秒
?************************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#define PORT 8888
#define MAXDATASIZE 2048
int main(int argc, char *argv[])
{
? ? if(argc != 2)
? ? {
? ? ? ? fprintf(stderr, "老鐵,請輸入想連接的服務器IP地址!\n");
? ? ? ? exit(1);
? ? }
? ? int sockfd;
? ? sockfd = socket(AF_INET, SOCK_STREAM, 0);
? ? const char *server_ip = argv[1]; //從命令行獲取輸入的ip地址
? ? struct sockaddr_in serveraddr;
? ? bzero(&serveraddr, sizeof(serveraddr));
? ? serveraddr.sin_family = AF_INET;
? ? serveraddr.sin_port = htons(PORT);
? ? inet_pton(AF_INET, server_ip, &serveraddr.sin_addr);
? ? connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
? ? printf("=====================服務器鏈接成功=====================\n");
? ? char buf[MAXDATASIZE];
? ? memset(buf, 0 ,? sizeof(buf));
? ??printf("input: ");
? ? while(fgets(buf, sizeof(buf), stdin) != NULL && (strcmp(buf, "quit")))
? ? {
? ? ? ? send(sockfd, buf, sizeof(buf), 0);
? ? ? ? memset(buf, 0, sizeof(buf));
? ? ? ? recv(sockfd, buf, sizeof(buf), 0);
? ? ? ? printf("server say: ");
? ? ? ? fputs(buf, stdout);
? ? ? ? memset(buf, 0, sizeof(buf));
? ? ? ? printf("input: ");
? ? }
? ? printf("client will be closed, see you next time.\n");
? ? close(sockfd);
? ? return 0;
}
編譯連接
gcc -o cli cli.c &&?gcc -o ser ser.c
兩個終端上先運行服務器再運行客戶端
./ser
./cli 127.0.0.1
運行結果
cli:同學,走,一起去克利夫蘭支持詹姆斯干掉勇士隊!
ser:算了算了,我還要準備去準備網易云課堂上的【劍指BAT】安排的下一次網絡編程課程呢,等我加入了一線企業(yè),賺高薪了,哥帶你飛
cli:醬紫啊,哥哥要繼續(xù)加油哦,看好你,拜拜
ser:好滴,等我閉關修煉5個月,我們再見,拜拜


為了方便大家獲取源代碼,可以移步這里,?GitHub源代碼