目錄
一、主機字節序列和網絡字節序列
二、套接字地址結構
1、IPv4 地址結構 (sockaddr_in)
2、IPv6 地址結構 (sockaddr_in6)
3、通用套接字地址結構 (sockaddr)
4、Unix域套接字地址結構 (sockaddr_un)
5、專用 socket 地址結構
6、套接字地址結構的轉換
字符串轉二進制地址
二進制地址轉字符串
7、端口號的轉換
三、網絡編程接口
socket
bind
listen
accept
TCP 數據讀寫:
UDP 數據讀寫:
close
connect
四、TCP編程流程
代碼示例:
服務端
客戶端
一、主機字節序列和網絡字節序列
主機字節序列(Host Byte Order)指的是計算機在內存中存儲多字節數據時的順序。分為大端字節序和小端字節序,不同的主機采用的字節序列可能不同。
- 大端字節序是指一個整數的高位字節存儲在內存的低地址處,低位字節存儲在內存的高地址處。
- 小端字節序則是指整數的高位字節存儲在內存的高地址處,而低位字節則存儲在內存的低地址處。
在兩臺使用不同字節序的主機之間傳遞數據時,可能會出現沖突。所以,在將數據發送到網絡時規定整形數據使用大端字節序,所以也把大端字節序成為網絡字節序列。對方接收到數據后,可以根據自己的字節序進行轉換。
Linux 系統提供如下 4 個函數來完成主機字節序和網絡字節序之間的轉換:
#include <arpa/inet.h>
#include <netinet/in.h>uint32_t htonl(uint32_t hostlong); // 主機到網絡(長整型)長整型的主機字節序轉網絡字節序
uint16_t htons(uint16_t hostshort); // 主機到網絡(短整型)短整形的主機字節序轉網絡字節序
uint32_t ntohl(uint32_t netlong); // 網絡到主機(長整型)長整型的網絡字節序轉主機字節序
uint16_t ntohs(uint16_t netshort); // 網絡到主機(短整型)短整型的網絡字節序轉主機字節序
不同架構的處理器可能使用不同的字節序。x86架構通常是小端序,而網絡協議(如TCP/IP)要求使用大端序。因此,跨平臺通信時必須進行字節序轉換。
二、套接字地址結構
套接字地址結構用于在網絡編程中存儲通信所需的地址信息。常見的套接字地址結構包括IPv4、IPv6和Unix域套接字地址結構。
1、IPv4 地址結構 (sockaddr_in)
IPv4套接字地址結構定義在<netinet/in.h>
中,用于存儲IPv4地址和端口號。
struct sockaddr_in {sa_family_t sin_family; // 地址族,如AF_INETin_port_t sin_port; // 16位端口號,網絡字節序struct in_addr sin_addr; // 32位IPv4地址,網絡字節序char sin_zero[8]; // 填充字段,通常置零
};struct in_addr {uint32_t s_addr; // IPv4地址,網絡字節序
};
2、IPv6 地址結構 (sockaddr_in6)
IPv6套接字地址結構用于存儲IPv6地址和端口號。
struct sockaddr_in6 {sa_family_t sin6_family; // 地址族,如AF_INET6in_port_t sin6_port; // 16位端口號,網絡字節序uint32_t sin6_flowinfo; // 流信息struct in6_addr sin6_addr; // 128位IPv6地址,網絡字節序uint32_t sin6_scope_id; // 范圍ID
};struct in6_addr {unsigned char s6_addr[16]; // IPv6地址,網絡字節序
};
3、通用套接字地址結構 (sockaddr)
socket 網絡編程接口中表示 socket 地址的是結構體 sockaddr,通用套接字地址結構用于在函數參數中傳遞不同類型的地址結構。其定義如下:
struct sockaddr {sa_family_t sa_family; // 地址族char sa_data[14]; // 協議地址
};
4、Unix域套接字地址結構 (sockaddr_un)
Unix域套接字用于本地進程間通信,其地址結構定義在<sys/un.h>
中。
struct sockaddr_un {sa_family_t sun_family; // 地址族,AF_UNIXchar sun_path[108]; // 文件路徑名
};
5、專用 socket 地址結構
/*
sin_family: 地址族 AF_INET
sin_port: 端口號,需要用網絡字節序表示
sin_addr: IPV4 地址結構:s_addr 以網絡字節序表示 IPV4 地址
*/struct in_addr{u_int32_t s_addr;
};
struct sockaddr_in{sa_family_t sin_family;u_int16_t sin_port;struct in_addr sin_addr;
};
struct in6_addr{unsigned char sa_addr[16]; // IPV6 地址,要用網絡字節序表示
};
struct sockaddr_in6{sa_family_t sin6_family; // 地址族:AF_INET6u_inet16_t sin6_port; // 端口號:用網絡字節序表示u_int32_t sin6_flowinfo; // 流信息,應設置為 0struct in6_addr sin6_addr; // IPV6 地址結構體u_int32_t sin6_scope_id; // scope ID,尚處于試驗階段
};
6、套接字地址結構的轉換
在網絡編程中,通常需要將人類可讀的IP地址和端口號轉換為網絡字節序的二進制形式。
字符串轉二進制地址
使用inet_pton
函數將點分十進制字符串轉換為二進制地址。
#include <arpa/inet.h>const char *ip_str = "192.168.1.1";
struct in_addr addr;
inet_pton(AF_INET, ip_str, &addr);
二進制地址轉字符串
使用inet_ntop
函數將二進制地址轉換為點分十進制字符串。
char ip_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &addr, ip_str, INET_ADDRSTRLEN);
7、端口號的轉換
端口號需要轉換為網絡字節序(大端序)后使用。
#include <arpa/inet.h>uint16_t port = 8080;
uint16_t net_port = htons(port); // 主機字節序轉網絡字節序
uint16_t host_port = ntohs(net_port); // 網絡字節序轉主機字節序
三、網絡編程接口
-
socket
socket()創建套接字,成功返回套接字的文件描述符,失敗返回-1
domain: 設置套接字的協議簇, AF_UNIX AF_INET AF_INET6 ;type: 設置套接字的服務類型 SOCK_STREAM SOCK_DGRAM ;protocol: 一般設置為 0,表示使用默認協議
int socket(int domain, int type, int protocol);
-
bind
bind()將 sockfd 與一個 socket 地址綁定,成功返回 0,失敗返回-1
sockfd 是網絡套接字描述符 ;addr 是地址結構 ;addrlen 是 socket 地址的長度
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
listen
listen()創建一個監聽隊列以存儲待處理的客戶連接,成功返回 0,失敗返回-1
sockfd 是被監聽的 socket 套接字 ;backlog 表示處于完全連接狀態的 socket 的上限
int listen(int sockfd, int backlog);
-
accept
accept()從 listen 監聽隊列中接收一個連接,成功返回一個新的連接 socket,該 socket 唯一地標識了被接收的這個連接,失敗返回-1
sockfd 是執行過 listen 系統調用的監聽 socket ;addr 參數用來獲取被接受連接的遠端 socket 地址 ;addrlen 指定該 socket 地址的長度
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
TCP 數據讀寫:
recv()讀取 sockfd 上的數據,buff 和 len 參數分別指定讀緩沖區的位置和大小
send()往 socket 上寫入數據,buff 和 len 參數分別指定寫緩沖區的位置和數據長度
flags 參數為數據收發提供了額外的控制
ssize_t recv(int sockfd, void *buff, size_t len, int flags);ssize_t send(int sockfd, const void *buff, size_t len, int flags);
-
UDP 數據讀寫:
recvfrom()讀取 sockfd 上的數據,buff 和 len 參數分別指定讀緩沖區的位置和大小
src_addr 記錄發送端的 socket 地址 ;addrlen 指定該地址的長度 ;sendto()往 socket 上寫入數據,buff 和 len 參數分別指定寫緩沖區的位置和數據長度 ;dest_addr 指定接收數據端的 socket 地址 ;addrlen 指定該地址的長度
ssize_t recvfrom(int sockfd, void *buff, size_t len, int flags,struct sockaddr* src_addr, socklen_t *addrlen);ssize_t sendto(int sockfd, void *buff, size_t len, int flags,struct sockaddr* dest_addr, socklen_t addrlen);
-
close
close()關閉一個連接,實際上就是關閉該連接對應的 socket
int close(int sockfd);
-
connect
connect()客戶端需要通過此系統調用來主動與服務器建立連接,成功返回 0,失敗返回-1
sockfd 參數是由 socket()返回的一個 socket。 serv_addr 是服務器監聽的 socket 地址 ;addrlen 則指定這個地址的長度
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
四、TCP編程流程
TCP 提供的是面向連接的、可靠的、字節流服務。TCP 的服務器端和客戶端編程流程如下:
- socket()方法是用來創建一個套接字,有了套接字就可以通過網絡進行數據的收發。這也是為什么進行網絡通信的程序首先要創建一個套接字。創建套接字時要指定使用的服務類型,使用 TCP 協議選擇流式服務(SOCK_STREAM)。
- bind()方法是用來指定套接字使用的 IP 地址和端口。IP 地址就是自己主機的地址,如果主機沒有接入網絡,測試程序時可以使用回環地址“127.0.0.1”。端口是一個 16 位的整形值,一般 0-1024 為知名端口,如 HTTP 使用的 80 號端口。這類端口一般用戶不能隨便使用。其次,1024-4096 為保留端口,用戶一般也不使用。4096 以上為臨時端口,用戶可以使用。在Linux 上,1024 以內的端口號,只有 root 用戶可以使用。
- listen()方法是用來創建監聽隊列。監聽隊列有兩種,一個是存放未完成三次握手的連接,一種是存放已完成三次握手的連接。listen()第二個參數就是指定已完成三次握手隊列的長度。
- accept()處理存放在 listen 創建的已完成三次握手的隊列中的連接。每處理一個連接,則accept()返回該連接對應的套接字描述符。如果該隊列為空,則 accept 阻塞。
- connect()方法一般由客戶端程序執行,需要指定連接的服務器端的 IP 地址和端口。該方法執行后,會進行三次握手, 建立連接。
- send()方法用來向 TCP 連接的對端發送數據。send()執行成功,只能說明將數據成功寫入到發送端的發送緩沖區中,并不能說明數據已經發送到了對端。send()的返回值為實際寫入到發送緩沖區中的數據長度。
- recv()方法用來接收 TCP 連接的對端發送來的數據。recv()從本端的接收緩沖區中讀取數據,如果接收緩沖區中沒有數據,則 recv()方法會阻塞。返回值是實際讀到的字節數,如果 recv()返回值為 0, 說明對方已經關閉了 TCP 連接。
- close()方法用來關閉 TCP 連接。此時,會進行四次揮手。
代碼示例:
服務端
編輯
?
?
代碼示例:
服務端:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>int main(){
int sockfd = socket(AF_INET,SOCK_STREAM,0);//創建套接字
if( sockfd == -1 ){
exit(1);
}
struct sockaddr_in saddr;//定義ipv4地址,
memset(&saddr,0,sizeof(saddr));//清空
saddr.sin_family = AF_INET;//填充地址圖
saddr.sin_port = htons(6000);//填充端口,大端
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");//ip地址
int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//綁定
if( res == -1){
printf("bind err\n");
exit(1);
}
res = listen(sockfd,5);//創建監聽隊列
if( res == -1){
exit(1);
}
while( 1 ){
struct sockaddr_in caddr;//記錄客戶端地址
socklen_t len = sizeof(caddr);//計算大小
int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//接受套接字,阻塞
char buff[128] = {0};
int num = recv(c,buff,127,0);//read
printf("buff=%s\n",buff);//接收之后,打印
send(c,"ok",2,0);//write,描述符,帶發送的數據,大小,標志位
close(c);
}
close(sockfd);
exit(0);
}
客戶端
客戶端:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
int main(){
int sockfd = socket(AF_INET,SOCK_STREAM,0);//創建套接字
if( sockfd == -1){
exit(1);
}
struct sockaddr_in saddr;//ipv4地址
memset(&saddr,0,sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(6000);
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
if( res == -1){
printf("connect err\n");
exit(1);
}
printf("input:\n");
char buff[128] = {0};
fgets(buff,128,stdin);//從鍵盤獲取數據
send(sockfd,buff,strlen(buff)-1,0);
memset(buff,0,128);
recv(sockfd,buff,127,0);
printf("buff=%s\n",buff);
close(sockfd);
exit(0);
}
執行結果: