網絡編程自己是一門很大的學問,涉及到的東西也不少,尤爲是各類協議。先看圖:
正如上圖所示,網絡編程中包含五大層面(也有區分六個層面),從應用層到物理層能夠明顯看出 越往下越接近計算機硬件。本身並非專業網絡編程的工程師,因此僅對這五大層面有一點點粗淺的瞭解,這篇文章網絡編程技巧博主寫的比較詳細. 平時大多數所謂網絡編程,實際上是在傳輸層、網絡層
方面.html
首先,socket(套接字)編程應該屬於傳輸層
,主要實現的是端到端的通訊,很是相似於好久好久之前的固話通訊,應用程序能夠經過它發送或者接受數據,能夠對它進行像文件似得讀寫、關閉等操做。套接字容許應用程序將I/O插入網絡中,並與網絡中的其餘應用程序進行通訊。編程
其次,網絡中兩臺或多臺主機之間進行通訊,必須知道對應主機的地址,也就是其IP地址,可是隻知道IP地址是遠遠不夠的,試想如你在本機A發送了一個消息,另外一個臺主機B也接收到到了該消息,可是究竟是B主機的哪個進程接受並處理該消息?就像你用QQ給B發送消息,可是B不可能經過陌陌收到該消息。 所以,相互通訊的主機之間還必須肯定一一對應的消息處理接口--端口。端口的存在,主要是爲了確認消息一一對應性。另外,端口號其實就是一個從0開始的到65535之間的一個整型數字,0~1023端口,也就是常說的靜態端口,已被操做系統另作它用(http,https,ftp等各類協議佔用),咱們本身所能使用的端口範圍只能從1024開始,即動態端口取值[1024,65535].服務器
但是看出,若要進行網絡間通訊,socket至少要包含IP+port兩個方面,其實事實也是如此.仍是以有線電話作爲類比,socket其實就是本身家中的一部電話,其中IP就是家庭地址,port就是本身家的電話號碼,當要給別人打電話時,別人家固然也必須有本身的座機和專屬於該座機的號碼.
或許咱們也能猜出,socket編程是網絡編程裏邊必不可少且及其重要的一個環節.網絡
熟悉Linux(這裏用Ubuntu16.04版本,其餘版本相似)下socket編程基本流程,掌握socket編程基本原理,搞懂Linux下socket編程所必須的函數及其用法.dom
實驗:在本地模擬兩臺機器,服務器和客戶端,服務器監聽客戶端信息並能發送廣播,客戶端能夠主動給服務器發送消息,其中消息的輸入是從標準輸入設備輸入,並輸出到標準輸出--Linux 終端.socket
開始以前必須瞭解一點 什麼是文件描述符,在Unix Linux系統中,文件描述符是一個非負整數,其存在做用更像一個索引,系統內核經過該"索引"找到對應的文件、設備、外設、安裝的軟件等等, 並經過描述符對它們進行操做。總而言之,文件描述符對應了系統上的全部文件,這裏的文件並不是"傳統意義上的普通文件",而是指Linux系統內核所能管理1的一切,包含文檔、文件、硬件設備、系統軟件等等。這也體現了Linux系統的設計思想----把一切視做文件.函數
既然socket
這麼重要,來看它究竟是個什麼東西.在Linux終端執行:man socket
,出現:
經過Linux手冊查詢能夠知道該函數所必須的頭文件,函數聲明和函數描述等信息.從[DESCRIPTION]字段可知,函數建立了一個用於通訊的端點並返回該端點的描述符,若建立成功,返回建立套接字的文件描述符,不然返回一負數.
函數聲明 int socket(int domain,int type,int protocol);
參數 domain
:表示建立該socket所使用的通信協議家族--地址族,如今通常用IPv4協議,因此一般會選擇AF_INET
;
參數type
:指定所需的通訊類型。包括數據流(SOCK_STREAM)<-->TCP協議、數據報(SOCK-DGRAM)<-->UDP協議和原始類型(S0CK_RAW)<-->新網格協議的開發測試.
參數protocol
:說明該套接字使用的協議族中的特定協議。若是不但願特別指定使用的協議,則置爲0,使用默認的鏈接模式.
若要進行 基於TCP IP的網絡開發測試,則函數建立方式通常爲:測試
int listenfd = socket(AF_INET,SOCK_STREAM,0);
既然有了一部「電話」,那麼就須要爲該電話綁定惟一的「所屬地址」,一樣Linux命令行執行:man bind
,一樣函數聲明爲:spa
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
從手冊的描述中能夠看出,當成功建立socket
套接字後,調用該函數能夠將所建立的套接字(sockfd)和指定的地址(addr)綁定.
地址是由這樣一個結構體指定:操作系統
struct sockaddr { sa_family_t sa_family; //地址族 char sa_data[14]; //14字節的協議地址 }
上面struct sockaddr
是通用地址,在網絡編程中 internet sockaddr使用下面地址,兩種地址能夠互換:
struct sockaddr_in { short int sin_family; /* 地址族,AF_xxx 在socket編程中只能是AF_INET */ unsigned short int sin_port; /* 端口號 (使用網絡字節順序) */ struct in_addr sin_addr; /* 存儲IP地址 4字節 */ unsigned char sin_zero[8]; /* 總共8個字節,實際上沒有什麼用,只是爲了和struct sockaddr保持同樣的長度 */ };
bind()函數的第三個參數表示地址所佔字節長度,socklen_t
本質上是一個 unsigned int
宏定義.
能夠經過這樣方式指定地址:
struct sockaddr_in serveraddr; memset(&serveraddr,0,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(5188); serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); //serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
首先聲明網絡接口地址結構,在給該地址賦值前必須將其清空.依次設置該地址的地址族、IP和端口(這裏隨便設置了一個),上邊出現另外一個新函數htons
,一樣終端下man htons
,可知該函數的主要做用是將主機字節序轉化爲網絡字節序,關於這兩個字節序後續再深刻研究.這裏能夠理解爲:htons()的主要做用就是將十進制的ip地址和端口號轉化爲網絡能夠識別的"東東".
至此,基本能夠完成座機的安裝入戶和號碼綁定:
bing(listenfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
對於咱們的服務器而言,它須要監聽來自客戶端發來的消息,Linix終端中 man listen
能夠看到詳細信息. 函數聲明爲:
int listen(int socdfd,int backlog);
其中參數sockfd
代指所要監聽的套接字文件描述符,參數backlog
表示在套接字掛起時,所能接受請求的最大隊列長度.函數執行成功返回 0,不然返回 -1.
必須說明一點,當調用該函數後,參數socdfd
所指定的套接字將變爲被動套接字
,所謂被動套接字,是指其只能用來接收來自其餘用戶的連接請求. 相似於改變了套接字的狀態,使其只能用於接收.
對於咱們的服務器而言,因爲其只具有接收功能,所以必須建立一個接受函數:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
函數參數不言自明,參數1sockfd表示服務器socket描述符,參數2是指客戶端的協議地址,參數3爲地址長度. 函數成功返回監聽的等待隊列中第一個套接字的描述符.
服務器的功能是監聽客戶端發來的消息,並將消息廣播給客戶端.所以須要一個循環實時監聽客戶端發來的消息,在本地構建一個簡單的服務器以下:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <fcntl.h> #include <errno.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #define ERR_EXIT(m) \ do \ { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) int main(void){ int listenfd; if(( listenfd = socket(PF_INET,SOCK_STREAM,0)) < 0){ ERR_EXIT("socket"); } struct sockaddr_in serveraddr; memset(&serveraddr,0,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(5188); serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); //serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if(bind(listenfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))<0) ERR_EXIT("bind"); //一旦監聽,則爲被動套接字(只能接受鏈接,調用accep函數以前調用),這裏隨便給了一個最大隊列長度 if(listen(listenfd,100)< 0) ERR_EXIT("listen"); //聲明一個地址,用於存儲客戶端連接時的協議地址 struct sockaddr_in peeraddr; socklen_t peerlen = sizeof(peeraddr); int conn; //返回的一個主動套接字 if((conn= accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0) ERR_EXIT("accept"); char recvbuff[1024]; while(1){ memset(recvbuff,0,sizeof(recvbuff)); int ret = read(conn,recvbuff,sizeof(recvbuff)); fputs(recvbuff,stdout); write(conn,recvbuff,ret); } close(listenfd); return 0; }
其中用到了幾個非socket API的函數:
ssize_t read(int fd,void *buf,size_t count); ssize_t write(int fd, const void *buf, size_t count);
read()
函數:負責從fd所指定文件描述符讀取字節大小爲count的數據到buf中.若成功返回實際讀取到的字節大小,不然返回負數,返回0表示讀取到文件結束.write()
:將buf中的count個字節內容寫入文件描述符fd.成功時返回寫的字節數.
客戶端的實現和服務器的實現之間大同小異,一樣都須要 」 安裝電話 「 ,可是客戶端的功能僅在於向外」撥打電話「. 區別在於客戶端是主動發起鏈接請求,因此它必須知道本身所要鏈接的目標,以後服務器纔有響應.一樣客戶端並不須要監聽,只須要接收到服務器的廣播便可. 發起鏈接請求須要函數 connect
:
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
在上述鏈接函數中,參數sockfd表示本機(客戶端)的socket套接字描述符,參數addr表示服務器端的地址,參數3表示地址長度.
代碼實現:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <fcntl.h> #include <errno.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #define ERR_EXIT(m) \ do \ { \ perror(m);\ exit(EXIT_FAILURE);\ }while(0) int main(void){ int sock; if(( sock = socket(PF_INET,SOCK_STREAM,0)) < 0){ ERR_EXIT("socket"); } struct sockaddr_in serveraddr; memset(&serveraddr,0,sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(5188); // serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //發起鏈接 connect(sock,(struct sockadddr*)&serveraddr,sizeof(serveraddr)); char recvbuf[1024]={0}; char sendbuf[1024]={0}; while(fgets(sendbuf,sizeof(sendbuf),stdin)!= NULL){ write(sock,sendbuf,strlen(sendbuf)); read(sock,recvbuf,sizeof(recvbuf)); fputs(recvbuf,stdout); memset(recvbuf,0,sizeof(recvbuf)); } close(sock); return 0; }
上述函數功能就是從客戶端主動向服務器發送鏈接請求,並在客戶端機器的標準設備上如字符,服務器接受並返回. 實現兩臺機器通訊的模擬.
效果以下圖:
用gcc
編譯上述兩個文件,首先啓動服務器,以後啓動客戶端.在客戶端隨便輸入字符,服務器解收到並廣播返回. 至此基本完成目的.
目前來看,建立服務器的通常流程是:
1.建立socket套接字(`socket`函數); 2.建立服務器地址,地址包含協議族、IP和端口號(`const struct sockaddr*`); 3.綁定套接字和服務器地址(bind函數); 4.系統監聽服務器,一旦監聽則該套接字變爲被動套接字,只能用於接收數據(`listen`函數); 5.做爲服務器,應該能接收客戶端信息(`accept`函數),該函數返回一個主動套接字;
基於以上步驟,基本能搭建一個簡單的服務器.
客戶端的搭建相比而言簡單許多:
1.建立用於鏈接的套接字; 2.將套接字和服務器地址鏈接; 3.發送消息