Linux C語言socket網絡編程
需要Linux C 服務器開發視頻學習資料的朋友請後臺私信【架構】獲取
注意:本文是按照 TCP、UDP的工作過程進行總結的
- TCP套 socket 接口編程: 基於TCP的 客戶/服務器(C/S)模式的工作過程如下:
服務器進程中的一些函數:
- socket():
/* 函數所需頭文件及其原型 */
#include
int socket( int family, int type, int protocol);
socketfd = soket(AF_INET, SOCK_STREAM, 0);
/* socketfd 作為返回值,可以記作描述符。
若 socketfd 非負則表示成功,為負則表示失敗。
參數:
family -> 指明協議族
type -> 字節流類型
protocol -> 一般置0.
參數 family 的取值範圍是:
AF_LOCAL UNIX 協議族
AF_ROUTE 路由器接口
AF_INET IPv4 協議
AF_INET6 IPv6 協議 AF_KEY 密鑰套接口
參數 type 的取值範圍:
SOCK_STREAM TCP 套接口
SOCK_DGRAM UDP 套接口
SOCK_PACKET 支持數據鏈路訪問
SOCK_RAM 原始套接口
*/
生成套接口描述字(套接字)後,要為套接口的地址數據結構進行賦初值。
通用套接口地址的數據結構中,struct sockaddr_in 需要掌握:
struct in_addr {
in_addr_t s_addr;
/*32 位 IP 地址,網絡字節序*/
};
struct sockaddr_in {
uint8 sin_len;
sa_family_t sin_family;
in_port_t sin_port;
/*16 位端口號,網絡字節序*/
struct in_addr sin_addr;
char sin_zero[8];
/*備用的域,未使用*/
};
PS:需要注意的是,一般在 socket() 之後,我們會填寫 sockaddr 的相關內容。
/* Fill the local socket address struct */
memset (&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // Protocol Family
servaddr.sin_port = htons (PORT); // Port number
servaddr.sin_addr.s_addr = htonl (INADDR_ANY); // AutoFill local address
- bind():
<code> // 函數原型:
#include/<code>
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
/*
參數 sockfd :套接字描述符。
參數 my_addr:指向 sockaddr 結構體的指針(該結構體中保存有端口和 IP 地址 信息)。
參數 addlen:結構體 sockaddr 的長度。
返回:0──成功, -1──失敗
*/
ret = bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct sockaddr));
/* 功能:當調用 socket 函數創建套接字後,該套接字並沒有與 本機地址和端口等 信息相連,
bind 函數將完成這些工作。
*/
- listen():
<code> // 函數原型:
#include/<code>
#include
// #define BACKLOG 10
int listen(int sockfd,int backlog);
/*
參數 sockfd :套接字描述符。
參數 backlog :規定內核為此套接口排隊的最大選擇個數。
*/
ret = listen(sockfd,BACKLOG);
// 通常採用一下的異常處理:
if(listen(listenfd,BACKLOG) == -1){
printf("ERROR: Failed to listen Port %d.\\n", PORT);
return (0);
}
else{
printf("OK: Listening the Port %d sucessfully.\\n", PORT);
}
處在監聽模式下後,程序就需要一個循環來實現掛起等待客戶機請求。所以接下來的一步就是 接受客戶機的請求。
- accept():
先來了解一下 accept() 這個函數:
<code>// 函數原型:
#include/<code>
#include
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);
/*
sockfd 參數:監聽的 套接字描述符。
cliaddr 參數:指向結構體 sockaddr 的指針。
addrlen 參數:cliaddr 參數指向的內存空間的長度。
*/
sin_size = sizeof(struct sockaddr_in);
connect_fd = accept(sockfd,( struct sockaddr *)&their_addr,&sin_size);
accept() 函數用於面向連接類型的套接字類型。 accept() 函數將從連接請求隊列中獲得連接信息,創建新的套 接字,並返回該套接字的文件描述符。 新創建的套接字用於服務器與客戶機的通信,而原來的套接字仍然處於監聽狀態。 它們的區別在於:監聽套接口描述字 只有一個,而且一直存在, 每一個連接都有一個已連接套接口描述字,當連接斷開 時就關閉該描述字。
注意:bind 函數和 accept 函數的第三個參數是不一樣的。
- close():
<code>// 函數原型:
#include <unistd.h>
int close(int sockfd);
// 成功則返回 0,否則返回-1。
// 功能:關閉套接口 其中參數 sockfd 是關閉的套接口描述字。
// 當對一個套接口調用 close()時, 關閉該套接口描述字,並停止連接。
以後這個套接口不能再使用,也不能再執行 任何讀寫操作,但關閉時已經排隊準備發送的數據仍會被髮出 使用完一個套接口後,一定要記得將它關掉,任何一個文件讀寫操作完畢之後, 都要關閉它的描述字。
/<unistd.h>/<code>
客戶機進程中的一些函數:
- socket(): 這個函數前面提過,這裡不必多說。 創建套接字後,同理,也需要對套接口進行設置: (這是在客戶端填充的服務器 端的資料)...... bzero(&server_addr,sizeof(server_addr)); // 初始化,置 0 server_addr.sin_family=AF_INET; // IPV4 server_addr.sin_port=htons(portnumber); // (將本機器上的 short 數據轉化為網絡上的 short 數據)端口號,與服務器端 的端口號相同 server_addr.sin_addr=*((struct in_addr *)host->h_addr_list); // IP 地址
- connect():
<code> connect(sockfd,(struct sockaddr *)(&server_addr), sizeof(structsockaddr));
函數原型:
#include/<code>
#include
int connect(int sockfd,const struct sockaddr *serv_addr,int addrlen);
/*
返回值:成功:返回 0 錯誤:返回-1,並將全局變量 errno 設置為相應的錯誤號。
參數 sockfd :數據發送的套接字,解決從哪裡發送的問題,ockfd 是先前 socket 返回的值
參數 serv_addr:據發送的目的地,也就是服務器端的地址
參數 addrlen:指定 server_addr 結構體的長度
*/
函數功能:
創建了一個套接口之後,使客戶端和服務器連接。其實就是完成一個 有連接協議 的連接過程,
對於 TCP 來說就是那個三段握手過程。
關於三段握手:( 《計算機網絡》謝希仁編著 第七版中 將其定名為:" 三報文握手 "):
客戶端先用 connect() 向服務器發出一個要求連接的信號 SYN1;
服務器 進程接收到這個信號後,發回應答信號 ack1,同時這也是一個要求回答的信號 SYN2;
客戶端收到信號 ack1 和 SYN2 後,再次應答 ack2; 服務器收到應答信號 ack2,一次連接才算建立完成。
從上面過程可以看出,服務器會收到兩次信 號 SYN1 和 ack2,因此服務器進程需要兩個隊列保存不同狀態的連接。剛接收 到 SYN1 信號時,連接還未完成,這時的連接放在一個名為“未完成連接”的隊列中。接收到 ack2 信號後,三段握手完成,這時的連接放在名為“已完成連接” 的隊列中,等待 accept() 調用。
關於 recv() 、send() 和 recvfrom() 、sendto() :
- 先說前兩個:
recv() 和 send() 都是基於 TCP 協議。
不論是客戶還是服務器應用程序都用send函數來向TCP連接的另一端發送數據。
客戶程序一般用send函數向服務器發送請求,而服務器則通常用send函數來向客戶程序發送應答。
同樣,不論是客戶還是服務器應用程序都用recv函數從TCP連接的另一端接收數據。
// 函數原型:
int send( SOCKET s, const char *buf, int len, int flags );
int recv( SOCKET s, char *buf, int len, int flags );
(1)recv 先等待 s 的發送緩衝中的數據被協議傳送完畢,如果協議在傳送 s 的發送緩衝中的數據時出現網絡錯誤,那麼recv函數返回 SOCKET_ERROR ;
(2)如果 s 的發送緩衝中沒有數據或者數據被協議成功發送完畢後,recv 先檢查套接字 s 的接收緩衝區,如果 s 接收緩衝區中沒有數據或者協議正在接收數據,那麼 recv 就一直等待,直到協議把數據接收完畢。
當協議把數據接收完畢,recv 函數就把 s 的接收緩衝中的數據 copy 到 buf 中(注意協議接收到的數據可能大於 buf 的長度,所以在這種情況下要調用幾次
recv 函數才能把s的接收緩衝中的數據 copy 完。recv 函數僅僅是 copy 數據,真正的接收數據是協議來完成的); 其中,recv 函數返回其實際 copy 的字節數。如果 recv 在 copy 時出錯,那麼它返回 SOCKET_ERROR;如果recv函數在等待協議接收數據時網絡中斷了,那麼它返回 0。
- 然後是後兩個:
recvfrom() 和 sendto() 都是基於 UDP 協議。
不同於 TCP 協議,UDP 提供的是一種無連接的、不可靠的數據包協議。它不對數據進行確認、出錯重傳、排序等可靠性處理,但是它卻具有
代碼小、速度快和系統開銷小等優點。對於某些應用程序,使用 UDP 來實現,將帶來更大效率。 與基於 TCP 協議的客戶機/服務器模式的工作流程圖相比較,它們的主要區別 在於:
使用 TCP 套接口必須先建立連接(例如客戶進程的 connect() ,服務器進程 的 **listen() **和 accept() ) 。
而 UDP 套接口不需預先連接,它在調用 socket()生成一個套接口後,
-> 在服務器端調用 bind() 綁定眾所周知的端口後, 服務器阻塞於 recvfrom() 調用,
-> 客戶端調用 sendto() 發送數據請求,阻塞於 recvfrom() 調用,
-> 服務器端調用 recvfrom() 接收數據,服務器端也調用 sendto() 向客戶發送數據作為應答,然後阻塞於 recvfrom() 調用,
-> 客戶端 調用 recvfrom() 接收數據......
當數據傳輸完成以後,UDP 套接口中的客戶端調用 close() 斷開連接,而 TCP 套接口中的客戶端不必再發出“斷開連接信號”來通知服務器端關閉連接。
一些重要的應用程序,如域名服務系統 DNS、網絡文件 系統 NFS 都使用 UDP 套接口。
// 函數原型:
#include
int recvfrom(int sockfd, void *buff, int len,int flags, struct sockaddr *fromaddr, int *addrlen);
/*
參數 sockfd 為套接口描述字;
參數 buff 為指向讀緩衝的指針;
參數 len 為讀的字節數;
參數 flags 一般設置為 0;
參數 fromaddr 為指向數據接收的套接口地址結構的指針;
參數 addrlen 為套接口結構長度。
函數返回實際讀的字節數,可以為 0,如果出錯,則返回-1。
*/
int sendto(int sockfd, void *mes,int len, int flags, struct sockaddr *toaddr, int *addrlen);
/*
參數 mes 為指向寫緩衝的指針;
參數 toaddr 為指向數據發送的套接口地址結構的指針;
函數返回實際寫的字節數,可以為 0,如果出錯,則返回-1。
*/
當真正開始學習的時候難免不知道從哪入手,學習時頻繁踩坑,導致效率低下影響繼續學習的信心,最終浪費大量時間。為了讓學習變得輕鬆、高效!今天給大家免費分享一套教學資源,幫助大家在成為架構師的道路上披荊斬棘。查看我的主頁即可~
分享主要有C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK技術,面試技巧方面的資料技術討論。
閱讀更多 Hu先生Linux後臺開發 的文章