Linux C Socket 程式設計
1 Socket 是什麼
Socket(套接字),就是對 網路上程序通訊 的 端點 的 抽象。一個 Socket 就是網路上程序通訊的一端,提供了應用層程序利用網路協議交換資料的機制。
從所處的位置來講,套接字上聯應用程序,下聯網路協議棧,是應用程式通過網路協議進行通訊互動的介面。如下圖所示:
2 Socket 型別
2.1 標準套接字
標準套接字是在傳輸層使用的套接字,分為流式套接字(SOCK_STREAM)和資料報套接字(SOCK_DGRAM)。
標準套接字在接收和傳送時只能操作資料部分(TCP Payload / UDP Payload),而不能對 IP 首部或TCP 首部和 UDP 首部進行操作。
2.1.1 流套接字(SOCK_STREAM)
流套接字(SOCK_STREAM)用於提供 面向連線(可靠)的資料傳輸服務。
流套接字保證資料能夠實現無差錯、無重複發資料,並按順序接收。
流套接字(SOCK_STREAM)使用 TCP(The Transmission Control Protocol)協議 進行資料的傳輸。
2.1.2 資料報套接字(SOCK_DGRAM)
資料報套接字(SOCK_DGRAM)用於提供 無連線(不可靠)的資料傳輸服務。
資料報套接字不保證資料傳輸的可靠性,資料有可能在傳輸過程中丟失或出現數據重複,且無法保證順序地接收到資料。
資料報套接字(SOCK_DGRAM)使用 UDP
2.2 原始套接字(SOCK_RAW)
原始套接字(SOCK_RAW)可以做到標準套接字做到的事,更可以做到標準套接字做不到的事。
原始套接字是在傳輸層及傳輸層以下使用的套接字。
原始套接字在接收和傳送時不僅能操作資料部分(TCP Payload / UDP Payload),也能對 IP 首部或TCP 首部和 UDP 首部進行操作。
因此如果我們開發的是更底層的應用,比如傳送一個自定義的 IP 包、UDP 包、TCP 包或 ICMP 包,捕獲所有經過本機網絡卡的資料包(sniffer),偽裝本機的 IP ,拒絕服務攻擊(DOS)等,都可以通過原始套接字(SOCK_RAW)實現。
注意:必須在管理員許可權下才能使用原始套接字。
3 Socket() 函式 介紹
3.1 功能
分配檔案描述符,建立 socket,即建立網路上程序通訊的端點。
3.2 標頭檔案
#include <sys/types.h>
#include <sys/socket.h>
3.3 函式原型
int socket(int domain, int type, int protocol)
3.4 引數
注意:type 和 protocol 不可以隨意組合,如 SOCK_STREAM 不可以跟 IPPROTO_UDP 組合。
具體的組合和應用場景可以參考 4 建立 Socket 及其應用場景
3.4.1 domain
domain:即協議域,又稱為協議族(family),如下所示:
-
AF_INET / PF_INET(2):IPv4,獲取 網路層的資料
-
AF_INET6:IPv6
-
AF_UNIX:UNIX 系統本地通訊
-
AF_PACKET / PF_PACKET(17):乙太網包,獲取 資料鏈路層的資料
注:
-
AF = Address Family(地址族),PF = Protocol Family(協議族)
-
理論上建立 socket 時是指定協議,應該用 PF_xxxx,設定地址時應該用 AF_xxxx。當然 AF_xxxx和 PF_xxxx 的值是相同的,混用也不會有太大的問題。
3.4.2 type
type:指定 socket 型別,如下所示:
-
SOCK_STREAM(1):面向連線的流式套接字(TCP)
-
SOCK_DGRAM(2):面向無連線的資料包套接字(UDP)
-
SOCK_RAW(3):接收 底層資料報文 的原始套接字
-
SOCK_PACKET(10):過時型別,可以使用,但是已經廢棄,以後不保證還能支援,不推薦使用。
3.4.3 protocol
protocol:指定協議,如下所示:
-
0:自動選擇 type 型別對應的預設協議。
-
IPPROTO_IP(0):接受 TCP 型別的資料幀
-
IPPROTO_ICMP(1):接受 ICMP 型別的資料幀
-
IPPROTO_IGMP(2)接受 IGMP 型別的資料幀
-
IPPROTO_TCP(6):接受 TCP 型別的資料幀
-
IPPROTO_UDP(17):接受 UDP 型別的資料幀
-
ETH_P_IP(0x800):接收發往本機 MAC 的 IP 型別的資料幀
-
ETH_P_ARP(0x806):接受發往本機 MAC 的 ARP 型別的資料幀
-
ETH_P_RARP(0x8035):接受發往本機 MAC 的 RARP 型別的資料幀
-
ETH_P_ALL(0x3):接收發往本機 MAC 的所有型別 IP ARP RARP 的資料幀,接收從本機發出的所有型別的資料幀。(混雜模式開啟的情況下,會接收到非發往本地 MAC 的資料幀)
3.5 返回值
-
成功:返回一個檔案描述符
-
失敗:返回 -1,並設定 errno
3.6 備註
詳情檢視 man 手冊:man 2 socket
4 建立 Socket 及其應用場景
5 bind() 函式
5.1 功能
將 IP 地址資訊繫結到 socket。
5.2 標頭檔案
#include <sys/types.h>
#include <sys/socket.h>
5.3 函式原型
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
5.4 引數
5.4.1 sockfd
通訊 socket
5.4.2 addr
要繫結的地址資訊(包括IP地址,埠號)。
通用地址結構體定義:
struct sockaddr
{
sa_family_t sa_family; // 地址族, AF_xxx
char sa_data[14]; // 包括 IP 和埠號
}
新型的地址結構體定義:(檢視新型的結構體資訊: gedit /usr/include/linux/in.h )
struct sockaddr_in
{
__kernel_sa_family_t sin_family; // 地址族,IP 協議。預設:AF_INET
__be16 sin_port; // 埠號
struct in_addr sin_addr; // 網路 IP 地址
unsigned char __pad // 8 位的預留介面
};
5.4.3 addrlen
地址資訊大小
5.5 返回值
-
成功:返回 0
-
失敗:返回 -1,並設定 errno
5.6 備註
詳細檢視 man 手冊:man 2 bind
6 listen() 函式
6.1 功能
監聽指定埠,socket() 建立的 socket 是主動的,呼叫 listen 使得該 socket 成為 監聽 socket ,變主動為被動。
6.2 標頭檔案
#include <sys/socket.h>
6.3 函式原型
int listen(int sockfd, int backlog);
6.4 引數
6.4.1 sockfd
通訊 socket
6.4.2 backlog
同時能處理的最大連線要求
6.5 返回值
-
成功:返回 0
-
失敗:返回 -1,並設定 errno
6.6 備註
詳細檢視 man 手冊:man 2 listen
7 accept() 函式
7.1 功能
提取出 監聽 socket 的等待連線佇列中 第一個連線請求,建立 一個新的 socket,即 連線 socket
新建立的 連線 socket 用於傳送資料和接受資料。
7.2 標頭檔案
#include <sys/socket.h>
7.3 函式原型
#include <sys/types.h>
#include <sys/socket.h>
7.4 引數
7.4.1 sockfd
監聽 socket,即 在 呼叫 listen() 後的 監聽 socket。
7.4.2 addr
(可選)指標,指向一緩衝區,其中接收為通訊層所知的連線實體的地址。Addr引數的實際格式由套介面建立時所產生的地址族確定。
7.4.3 addrlen
(可選)指標,輸入引數,配合addr一起使用,指向存有addr地址長度的整型數。
7.5 返回值
-
成功:指向 新的 socket(連線 socket)的檔案描述符。
-
失敗:返回 -1,並設定 errno
7.6 備註
詳細檢視 man 手冊:man 2 listen
8 connect() 函式
8.1 功能
傳送連線請求
8.2 標頭檔案
#include <sys/types.h>
#include <sys/socket.h>
8.3 函式原型
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
8.4 引數
8.4.1 sockfd
通訊 socket
8.4.2 addr
要連線的伺服器地址
8.4.3 addrlen
地址資訊大小
8.5 返回值
-
成功:返回 0
-
失敗:返回 -1,並設定 errno
8.6 備註
詳細檢視 man 手冊:man 2 connect
9 sendto() 函式
9.1 功能
將資料由指定的 socket 傳給對方主機
9.2 標頭檔案
#include <sys/types.h>
#include <sys/socket.h>
9.3 函式原型
int sendto (int sockfd , const void * msg, int len, unsigned int flags, const
struct sockaddr * to , int tolen);
9.4 引數
9.4.1 sockfd
已建立連線的 socket,如果利用 UDP 協議則不需建立連線。
9.4.2 msg
傳送資料的緩衝區。
9.4.3 len
緩衝區長度。
9.4.4 flags
呼叫方式標誌位,一般設為 0 。
9.4.5 to
用來指定要傳送的網路地址,結構 sockaddr
9.4.6 tolen
sockaddr 的長度
9.5 返回值
-
成功:返回實際傳送出去的字元數
-
失敗:返回 -1,並設定 errno
9.6 備註
詳細檢視 man 手冊:man 2 sendto
10 recvfrom() 函式
10.1 功能
接收遠端主機經指定的 socket 傳來的資料,並把資料傳到由引數 buf 指向的記憶體空間。
10.2 標頭檔案
#include <sys/types.h>
#include <sys/socket.h>
10.3 函式原型
int recvfrom(int sockfd,void *buf,int len,unsigned int flags, struct sockaddr *from,int *fromlen);
10.4 引數
10.4.1 sockfd
已建立連線的 socket,如果利用 UDP 協議則不需建立連線。
10.4.2 buf
接收資料緩衝區。
10.4.3 len
緩衝區長度。
10.4.4 flags
呼叫方式標誌位,一般設為 0 。
10.4.5 from
(可選)指標,指向裝有源地址的緩衝區,結構 sockaddr
10.4.6 fromlen
(可選)指標,指向 from 緩衝區長度值,sockaddr 的結構長度
10.5 返回值
-
成功:返回實際接受到的字元數
-
失敗:返回 -1,並設定 errno
10.6 備註
詳細檢視 man 手冊:man 2 recvfrom
11 位元組序
位元組序,是 大於一個位元組型別的資料在記憶體中的存放順序,由 CPU 架構決定,與作業系統無關。是在跨平臺和網路程式設計中,時常要考慮的問題。
11.1 高低地址
在記憶體中,棧是向下生長的,以char arr[4]為例,(因為 char 型別資料只有一個位元組,不存在位元組序的問題)依次輸出每個元素的地址,可以發現,arr[0] 的地址最低,arr[3] 的地址最高,如圖:
11.2 高低位元組
在十進位制中靠左邊的是高位,靠右邊的是低位,在其他進位制也是如此。
例如: 0x12345678,從高位到低位的位元組依次是 0x12、0x34、0x56 和 0x78。
11.3 位元組序分類 - 大小端模式
位元組序被分為兩類:
-
大端模式(Big-endian):記憶體的 低地址 存放 資料的高位元組,記憶體的 高地址 存放 資料的低位元組。(與人類閱讀順序一致)
-
小端模式(Little-endian),是指記憶體的 低地址 存放 資料的低位元組,記憶體的 高地址 存放 資料的高位元組。
大端模式 CPU 代表是 IBM Power PC,小端模式 CPU 代表是 Intel X86、ARM。
11.4 大小端示例
以 0x12345678 為例,兩種模式在記憶體中的儲存情況,如下表所示:
11.5 判斷大小端
利用 C 語言 union 聯合體所有成員共用同一塊記憶體的特性,可以用聯合體快速實現判斷大小端。
#include <stdio.h>
union u
{
char c[4];
int i;
};
int main(void)
{
union u test;
int j;
test.i = 0x12345678;
for(j = 0; j < sizeof(test.c); j++)
{
printf("0x%x\n",test.c[j]);
}
return 0;
}
執行後結果:
可以看出,我的機器是小端位元組序。
11.6 網路位元組序與本機位元組序
網路位元組序(NBO,Network Byte Order),是 TCP/IP 中規定好的一種資料表示格式。它與具體的 CPU 型別、作業系統等無關,從而可以保證資料在不同主機之間傳輸時能夠被正確解釋。
網路位元組序採用大端(Big-endian)位元組序排序方式。
主機位元組順序(HBO,Host Network Order),與機器 CPU 相關,資料的儲存順序由 CPU 決定。
11.6.1 轉換函式
socket 程式設計中經常會用到 4 個網路位元組順序與本地位元組順序之間的轉換函式:htons()、ntohl()、 ntohs()、htons()。
htonl()--"Host to Network Long" // 長整型資料主機位元組順序轉網路位元組順序
ntohl()--"Network to Host Long" // 長整型資料網路位元組順序轉主機位元組順序
htons()--"Host to Network Short" // 短整型資料主機位元組順序轉網路位元組順序
ntohs()--"Network to Host Short" // 短整型資料網路位元組順序轉主機位元組順序
在使用小端位元組序的系統中,這些函式會把位元組序進行轉換。
在使用大端位元組序的系統中,這些函式會定義成空巨集。
12 程式碼示例
12.1 標準套接字(SOCK_STREAM - TCP)
12.1.1 TCP Socket 通訊過程
12.1.1.1 伺服器
1. 建立連線階段
-
呼叫 socket(),分配檔案描述符,建立 伺服器 socket
-
呼叫 bind(),將 socket 與本地 IP 地址和埠繫結
-
呼叫 listen(),監聽指定埠,socket() 建立的 socket 是主動的,呼叫 listen 使得該 socket 成為監聽 socket ,變主動為被動
-
呼叫 accept(),獲得 連線 socket,阻塞等待客戶端發起連線
2. 資料互動階段
-
呼叫 read(),阻塞等待客戶端傳送的資料請求,收到請求後從 read() 返回,處理客戶端請求
-
呼叫 write(),將資料傳送給客戶端
3. 關閉連線
- 當 read() 返回 0 的時候,說明客戶端發來了 FIN 資料包,即關閉連線,呼叫 close() 關閉 連線 socket 和 監聽 socket
12.1.1.2 客戶端
1. 建立連線階段
-
呼叫 socket(),分配檔案描述符,建立 客戶端 socket
-
呼叫 connect(),向伺服器傳送建立連線請求
2. 資料互動階段
-
呼叫 write(),向伺服器傳送資料
-
呼叫 read(),阻塞等待伺服器應答
3. 關閉連線
- 當沒有資料傳送的時候,呼叫 close() 關閉 客戶端 socket ,即關閉連線,向伺服器傳送 FIN 資料報
12.1.2 單個客戶端單個伺服器的 TCP 通訊
Linux-C TCP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子1
12.1.3 多執行緒實現 - 單個客戶端單個伺服器的 TCP 通訊
Linux-C TCP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子2
12.1.4 多路複用實現 - 單個客戶端單個伺服器的 TCP 通訊
Linux-C TCP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子3
12.1.5 多個客戶端單個伺服器的 TCP 通訊
Linux-C TCP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子4
12.1.6 多執行緒實現 - 多個客戶端單個伺服器的 TCP 通訊
Linux-C TCP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子5
12.1.7 多路複用實現 - 多個客戶端單個伺服器的 TCP 通訊
Linux-C TCP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144 - 例子6
12.2 標準套接字(SOCK_DGRAM- UDP)
12.2.1 UDP Socket 通訊過程
12.2.1.1 伺服器
- 建立連線階段
-
呼叫 socket(),分配檔案描述符,建立 伺服器 socket
-
呼叫 bind(),將 socket 與本地 IP 地址和埠繫結
- 資料互動階段
-
呼叫 recvfrom(),阻塞,接受客戶端的資料
-
呼叫 sendto(),將資料傳送給客戶端
- 關閉連線
- 呼叫 close() 關閉 伺服器 socket
12.2.1.2 客戶端
- 建立連線階段
- 呼叫 socket(),分配檔案描述符,建立 客戶端 socket
- 資料互動階段
-
呼叫 sendto(),向伺服器傳送資料
-
呼叫 recvfrom(),阻塞,接受伺服器的資料
- 關閉連線
- 呼叫 close() 關閉 客戶端 socket ,即關閉連線。
12.2.2 單個客戶端單個伺服器的 UDP 通訊
程式碼來源:Linux-C UDP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子1
12.2.3 多執行緒實現 - 單個客戶端單個伺服器的 UDP 通訊
程式碼來源:Linux-C UDP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子2
12.2.4 多路複用實現 - 單個客戶端單個伺服器的 UDP 通訊
程式碼來源:Linux-C UDP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子3
12.2.4 UDP 通訊組播
程式碼來源:Linux-C UDP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子4
12.2.4 UDP 通訊廣播
程式碼來源:Linux-C UDP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233 - 例子5
12.3 原始套接字
12.3.1 抓取乙太網上的所有資料幀
程式碼來源:GitHub - zhouyingjiu - https://github.com/zouyingjiu/sniffer
/*
* sniffer.c
*
* 功能:
* linux rawSocket 抓取乙太網上的所有資料幀
*
* 引數:
* 無
*
* 注意:
* 執行該程式需要 root 許可權 sudo ./
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef __linux__
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/ip_icmp.h>
#include <net/if_arp.h>
#include <netinet/if_ether.h>
#include <net/if.h>
#include <sys/ioctl.h>
#elif __win32__
#include <windows.h>
#endif
void UnpackARP(char *buff);
void UnpackIP(char *buff);
void UnpackTCP(char *buff);
void UnpackUDP(char *buff);
void UnpackICMP(char *buff);
void UnpackIGMP(char *buff);
int main(int argc, char **argv)
{
int sockfd, i;
char buff[2048];
/*
* 監聽乙太網上的所有資料幀
*/
if(0 > (sockfd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))))
{
perror("socket error!");
exit(-1);
}
while(1)
{
memset(buff, 0, 2048);
int n = recvfrom(sockfd, buff, 2048, 0, NULL, NULL);
printf("%s\n",buff);
printf("開始解析資料包============\n");
printf("大小: %d\n", n);
struct ethhdr *eth = (struct ethhdr*)buff;
char *nextStack = buff + sizeof(struct ethhdr);
int protocol = ntohs(eth->h_proto);
switch(protocol)
{
case ETH_P_IP:
UnpackIP(nextStack);
break;
case ETH_P_ARP:
UnpackARP(nextStack);
break;
}
printf("解析結束=================\n\n");
}
return 0;
}
void getAddress(long saddr, char *str)
{
sprintf(str, "%d.%d.%d.%d", \
((unsigned char*)&saddr)[0], \
((unsigned char*)&saddr)[1], \
((unsigned char*)&saddr)[2], \
((unsigned char*)&saddr)[3]);
}
void UnpackARP(char *buff)
{
printf("ARP資料包\n");
}
void UnpackIP(char *buff)
{
struct iphdr *ip = (struct iphdr*)buff;
char *nextStack = buff + sizeof(struct iphdr);
int protocol = ip->protocol;
char data[20];
getAddress(ip->saddr, data);
printf("來源ip %s\n", data);
bzero(data, sizeof(data));
getAddress(ip->daddr, data);
printf("目標ip %s\n", data);
switch(protocol)
{
case 0x06:
UnpackTCP(nextStack);
break;
case 0x17:
UnpackUDP(nextStack);
break;
case 0x01:
UnpackICMP(nextStack);
break;
case 0x02:
UnpackIGMP(nextStack);
break;
default:
printf("unknown protocol\n");
break;
}
}
void UnpackTCP(char *buff)
{
struct tcphdr *tcp = (struct tcphdr*)buff;
printf("傳輸層協議:tcp\n");
printf("來源埠:%d\n", ntohs(tcp->source));
printf("目標埠:%d\n", ntohs(tcp->dest));
}
void UnpackUDP(char *buff)
{
struct udphdr *udp = (struct udphdr*)buff;
printf("傳輸層協議:udp\n");
printf("來源埠:%d\n", ntohs(udp->source));
printf("目的埠:%d\n", ntohs(udp->dest));
}
void UnpackICMP(char *buff)
{
printf("ICMP資料包\n");
}
void UnpackIGMP(char *buff)
{
printf("IGMP資料包\n");
}
12.3.2 抓取乙太網上的所有資料幀,匹配 HTTP 協議併發送 TCP RST
程式碼來源:我的 Github - https://github.com/PikapBai/sniffer_cmpHTTP_sendTCP
13 參考資料
-
套接字 - 百度百科 - https://baike.baidu.com/item/套接字/9637606?fromtitle=socket&fromid=281150&fr=aladdin
-
RAW SOCKET - 百度百科 - https://baike.baidu.com/item/RAW SOCKET/995623?fromtitle=原始套接字&fromid=23692610&fr=aladdin#ref_[1]_4263346
-
原始套接字簡介 - chengqiuming - https://blog.csdn.net/chengqiuming/article/details/89577351
-
Linux 原始套接字抓取底層報文 - 2603898260 - https://blog.csdn.net/s2603898260/article/details/85020006?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param
-
Linux-C TCP 簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540144
-
【Linux網路程式設計】socket程式設計“網路位元組順序”和“主機位元組順序” - qq_20553613 - https://blog.csdn.net/qq_20553613/article/details/86385271
-
網路位元組序 - 百度百科 - https://baike.baidu.com/item/網路位元組序/12610557?fr=aladdin
-
位元組序(大小端)理解 - sunflower_della - https://blog.csdn.net/sunflower_della/article/details/90439935
-
理解大小端位元組序 - fan-yuan - https://www.cnblogs.com/fan-yuan/p/10406315.html
-
linux網路程式設計之TCP/IP的TCP socket通訊過程(含例項程式碼) - 知乎 - linux伺服器開發專欄 - https://zhuanlan.zhihu.com/p/148739946
-
Linux C Socket UDP程式設計詳解及例項分享 - 知乎 - linux伺服器開發專欄 - https://zhuanlan.zhihu.com/p/131402832
-
Linux-C UDP簡單例子 - nanfeibuyi - https://blog.csdn.net/nanfeibuyi/article/details/88540233
-
《圖解 TCP/IP》(第 5 版)[日]竹下隆史 /[日]村山公保/ [日]荒井透 / [日]苅田幸雄
-
淺談linux下原始套接字 SOCK_RAW 的內幕及其應用 - 知乎 - linux伺服器開發專欄 - https://zhuanlan.zhihu.com/p/254912774
-
GitHub - zhouyingjiu - https://github.com/zouyingjiu/sniffer