socket接口
int socket(int domain, int type, int protocol);
參數說明??
參數 | 說明 |
---|---|
domain | 協議族(地址族),如?AF_INET (IPv4)、AF_INET6 (IPv6) |
type | 套接字類型,UDP 使用?SOCK_DGRAM (數據報) |
protocol | 通常設為?0 (自動選擇),或?IPPROTO_UDP |
socket()
?的前兩個參數?domain
(地址族)和?type
(套接字類型)已經分別指定了??網絡層協議??(如 IPv4/IPv6)和??傳輸層協議??(如 TCP/UDP),第三個參數是歷史原因被留了下來,現在一般設為0
底層原理
socket系統調用會創建struct file,struct socket,struct sock等結構體,最終返回該套接字的文件描述符
struct sock
?
傳輸層和網絡層的底層實現
struct sock {struct sk_buff_head sk_receive_queue; // 傳輸層接收緩沖區struct sk_buff_head sk_write_queue; // 傳輸層發送緩沖區struct proto *sk_prot; // 純粹的傳輸層協議的操作集union {struct inet_sock inet; // IPv4的底層結構體struct ipv6_sock ipv6; // IPv6的底層結構體};// ...(定時器、擁塞控制、狀態等)
};
struct socket
對struct sock進行封裝,主要是封裝出了用戶級的系統調用操作集
struct socket {struct sock *sk; const struct proto_ops *ops; //協議相關的系統調用??(如?bind、connect、sendmsg)struct file *file;
};
操作集辨析
?1.file_operations
(struct file)?
??文件的通用操作接口??(如?read
、write
、poll
)
2.proto_ops(struct socket)
實現協議相關的系統調用??(如?bind
、connect
、sendmsg
)
3.?struct proto(struct sock)
傳輸層協議的底層操作集
??recvfrom
?函數聲明?
ssize_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen
);
參數詳解??
??參數?? | ??類型?? | ??說明?? |
---|---|---|
sockfd | int | 接受數據的套接字文件描述符(由?socket() ?創建)。 |
buf | void * | 接收數據的緩沖區地址,用于存儲接收到的數據。 |
len | size_t | 緩沖區的最大長度(字節數),防止緩沖區溢出。 |
flags | int | 控制接收行為的標志位(如?MSG_DONTWAIT 、MSG_PEEK ),通常設為?0 。 |
src_addr | struct sockaddr * | 接收發送方套接字地址結構體??的緩沖區 |
addrlen | socklen_t * | 傳入緩沖區的大小,返回套接字地址結構體大小。 |
返回值??
??返回值?? | ??說明?? |
---|---|
> 0 | 成功接收到的字節數。 |
0 | ??僅對 TCP 有效??,表示連接已關閉(UDP 不會返回?0 )。 |
-1 ?(失敗) | 出錯,可通過?errno ?獲取錯誤碼(如?EAGAIN ?表示非阻塞模式下無數據)。 |
sendto函數
ssize_t sendto(int sockfd, // 套接字文件描述符const void *buf, // 待發送數據的緩沖區size_t len, // 數據長度(字節數)int flags, // 發送方式控制標志(通常設為 0)const struct sockaddr *dest_addr, // 目標地址結構體socklen_t addrlen // 目標地址結構體長度
);
??參數解釋??
??參數?? | ??類型?? | ??說明?? |
---|---|---|
sockfd | int | 套接字文件描述符(由?socket() ?創建)。 |
buf | const void * | 待發送數據的緩沖區地址。 |
len | size_t | 數據的長度(字節數)。 |
flags | int | 控制發送行為的標志位(如?MSG_DONTWAIT 、MSG_MORE ),通常設為?0 。 |
dest_addr | const struct sockaddr * | ??目標地址結構體??(如?struct sockaddr_in )。 |
addrlen | socklen_t | dest_addr ?結構體的實際長度(如?sizeof(struct sockaddr_in) )。 |
?返回值??
??返回值?? | ??說明?? |
---|---|
> 0 | 成功發送的字節數。 |
-1 ?(失敗) | 出錯,可通過?errno ?獲取錯誤碼(如?EAGAIN ?表示非阻塞模式下無法立即發送)。 |
udp服務器邏輯
1.利用socket函數創建套接字,傳參地址族和套接字類型,第三個參數是歷史遺留不用管,比如socket(AF_INET, SOCK_DGRAM, 0),說明套接字網絡層是ipv4,傳輸層是tcp,其底層就是創建struct file還有對應的struct socket和struct sock,然后返回套接字描述符
2.接下來要將套接字綁定端口,創建ipv4對應的套接字地址結構體struct sockaddr_in,然后填寫地址族AF_INET,再填寫端口號和ip地址,一般端口號是靠命令行參數來給出,IP地址則是INADDR_ANY,該套接字綁定的ip是任意的,也就是通過任何一個網卡接收都可以。
3.然后服務器死循環調用recvfrom,recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);,沒有數據則在套接字接收緩沖區等待隊列上阻塞等待
udp客戶端邏輯
1.命令行參數指明服務器端主機的任意一個網卡的ip地址,然后再指明端口,創建套接字socket(AF_INET, SOCK_DGRAM, 0);
2.用命令行參數準備好服務器通信套接字的地址結構體struct sockaddr_in,然后sendto發消息
udp發送消息過程分析
udp是沒有發送緩沖區的,只有接收緩沖區,客戶端調用sendto,會自動給套接字綁定端口,然后直接開始封裝udp報文,然后交給IP層(其實就是調用ip協議的接口),傳的參數就是udp報文和目標ip地址,ip層查路由表確定下一跳ip和發送網卡接口,封裝IP報頭,交給數據鏈路層處理(本質是調用以太網協議接口),傳參IP報文和下一跳ip和發送網卡接口,網卡驅動會先查arp緩存,得到下一跳ip的mac地址,然后給報文加上mac頭和crc校驗,寫進發送網卡對應的發送緩沖區,寫網卡的TDT寄存器通知,然后網卡會DMA將數據讀出,HVY轉換信號,接口發送出去,咱們假設客戶端是內網的一個主機,服務器部署在外網主機上,那這個下一跳很明顯是路由器,路由器的網卡接口收到信號后,HVY信號轉換,DMA寫進網卡的接收緩沖區,觸發硬件中斷,cpu陷入內核,執行中斷向量表中的中斷方法,網卡驅動將數據讀出,檢查mac地址,然后看幀類型是IP幀,于是進行crc校驗,沒有問題就去掉mac頭和crc校驗,交給ip層,然后IP頭的TTL減一,更改源ip為路由器的WAN口ip,并根據ip頭中的首部長度,總長度,上層協議類型這些字段將udp頭中的源port也改了(路由器除了維護路由表,還會維護地址轉換表來輔助NAT,地址轉換表里的對應關系是{源ip,源port,目的ip,目的port}和{改過的源ip,改過的源port,目的ip,目的端口}),改完后去查路由表確定下一跳ip和發送網卡,然后和之前一樣發出去,這次發到公網了,服務器收到后不斷解包到傳輸層(此時的底層應該是調用了udp協議的接口,將udp報文傳過去),然后udp層會根據udp頭里的端口拼出三元組{協議,目的ip,目的端口},然后根據OS維護的hash表找到對應udp套接字,將udp報文整個寫進該套接字的接收緩沖區,然后喚醒接收緩沖區等待隊列上的進程,recvfrom系統調用從接收緩沖區中讀出一個完整的udp報文,然后去掉報頭,將有效載荷寫進recvfrom函數參數傳來的的應用層緩沖區里,并填充參數傳來的套接字地址和其大小