Linux socket API
socket是進程通信機制的一種,與PIPE、FIFO不同的是,socket即可以在同一臺主機通信(unix domain),也可以通過網絡在不同主機上的進程間通信(如:ipv4、ipv6),例如因特網,應用層通過調用socket API來與內核TCP/IP協議棧的通信,通過網絡字節實現不用主機之間的數據傳輸。
前置條件
字節序
對于多字節的數據,不同處理器存儲字節的順序稱為字節序,主要有大端序(big-endian)和小端序(little-endian),字節序的收發不統一就會導致值被解析錯誤。
大端序
高位字節存低位內存
大端序是最高位字節存儲在最低位內存地址處。例如一段數據0x0A0B0C0D,0x0A是最高位字節,0x0D是最地位字節,內存地址最低位a、最高位a+3,在大端序中存儲方式如下
- 8bit存儲方式:內存地址從低到高0x0A -> 0x0B -> 0x0C -> 0x0D
- 16bit存儲方式:內存地址從低到高0x0A0B -> 0x0C0D
小端序
低位字節存低位內存
小端序是最低位字節存儲在最低位內存地址處。例如一段數據0x0A0B0C0D,0x0A是最高位字節,0x0D是最地位字節,內存地址最低位a、最高位a+3,在小端序存儲方式如下
- 8bit存儲方式:內存地址從低到高0x0D->0X0C->0X0B->0X0A
- 16bit存儲方式:內存地址從低到高0X0C0D->0X0A0B
主機通常使用小端序,因為計算機先處理小端序的字節效率更高。通過上面的結構不難看出,大端序更易讀,所以網絡和存儲等采用了大端序,那么網絡通信的時候就需要將網絡字節的大端序轉換為主機字節的小端序。好在這些都有系統調用可以保證~
判斷主機的字節序:
#include <iostream>
using namespace std;
void byteorder() {
union {
short value;
char union_bytes[sizeof(short)];
} test;
test.value = 0x0102;
if ((test.union_bytes[0] == 0x01) && (test.union_bytes[1] == 0x02)) {
cout << "big endian" << endl; // [0x01, 0x02]
} else if ((test.union_bytes[0] == 0x02) && (test.union_bytes[1] == 0x01)) {
cout << "little endian" << endl; // [0x02, 0x01]
} else {
cout << "unknow~" << endl;
}
}
int main() { byteorder(); }
字節序轉換
#include<netinet/in.h>
// long型主機字節序轉換為long型網絡字節序, host to network
unsigned long int htonl(unsigned long int hostlong);
// short型
unsigned short int htons(unsigned short int hostshort);
// long型網絡字節序轉換為long型主機字節序, network to host
unsigned long int ntohl(unsigned long int netlong);
// short型
unsigned short int ntohs(unsigned short int netshort);
比方轉換主機的端口
int main(int argc, char *argv[]){
int port = atoi(argv[1]); // 主機序
server_address.sin_port = htons(port); // 網絡序
}
地址
通用地址
地址我們標識通信的端點,通用的地址格式為
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family; // 協議類型,例如 ipv4 AF_INET、unix AF_UNIX
char sa_data[14]; // unix域存放文件路徑,ip域存放ip地址和端口號
}
sa_data只能容納14字節地址數據,如果是unix域路徑長度可以達到108字節放不下,所以linux定義了新的地址
#include<bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int__ss_align; // 作用是內存對齊
char__ss_padding[128-sizeof(__ss_align)];
}
專有地址
專有地址在bind、accept、connect等需要用到的函數中需要強制轉換為通用地址,例如:(struct sockaddr *)&server_address
顧名思義專門為ipv4、unix、ipv6設計的不同socket地址結構,以ipv4為例
struct sockaddr_in
{
sa_family_t sin_family; // AF_INET
u_int16_t sin_port; // 網絡字節序的端口號
struct in_addr sin_addr; // IP地址
};
struct in_addr
{
u_int32_t s_addr; // 網絡字節序的IP地址
};
具體這樣用:
int main(int argc, char *argv[]) {
const char *ip = argv[1]; // 主機序ip地址
int port = atoi(argv[2]); // 主機序端口
struct sockaddr_in address; // ipv4專有地址
// 設置專有地址的成員
address.sin_family = AF_INET;
address.sin_port = htons(port);
// 將點分10進制的ip字符串轉換為網絡字節序整形表示的ip地址,存入sin_addr
inet_pton(AF_INET, ip, &address.sin_addr);
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 創建socket
// 綁定端口,要強制轉換為通用地址 (struct sockaddr *)&address
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
}
創建連接
創建socket
Linux一切皆文件,所以socket創建好之后就是一個文件描述符,對該fd讀寫關閉、屬性控制。
以ipv4為例
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- 第一個參數domain指定協議族,AF_INET、AF_UNIX、AF_INET6
- 第二個參數type指定socket類型,TCP\UDP分別使用流式SOCK_STREAM和數據報式SOCK_DGRAM
- 第三個參數protocal指定協議,有IPPROTO_TCP、IPPROTO_ICMP、IPPROTO_UDP等。通常使用默認的0。例如domain為AF_INET,type為SOCK_STREAM,那么就意味著ipv4 TCP類型的socket,protocal設置為0即可。
標識socket:bind
標識該socket,對于ipv4用ip地址和端口作為端點的表示
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
成功返回0,失敗返回-1并設置errno,例如errno
- EACCES:沒有權限綁定該端口
- EADDRINUSE:綁定一個沒有釋放的端口和地址,通常被處于TIME_WAIT的連接使用,需要使用
SO_REUSEADDR來復用處于TIME_WAIT連接的端口和地址
監聽socket:listen
開始監聽,并指定連接數
#include<sys/socket.h>
int listen(int sockfd,int backlog);
ret = listen(sock, 5);
- backlog參數表示處于ESTABLISHED狀態的連接數(我的ubuntu20.4測試為backlog+1),超過該值客戶端收到ECONNREFUSED或者客戶端TIMEOUT
接受連接:accept
從listen隊列中拿連接過來,不管該連接是ESTABLISED還是CLOSE_WAIT的狀態。
int connfd = accept(sockfd, (struct sockaddr *)&client, &client_addrlength);
發起連接:connect
connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address))
成功返回0,失敗返回-1并設置errno
- ECONNREFUSED:目標端口不存在或連接被拒絕
- ETIMEOUT:連接超時
關閉連接
close
關閉socket fd,默認情況下:如果是多進程,fork后會將fd引用計數加1,如果要關閉該socket,父子進程都需要close,而且是同時關閉讀和寫。可以通過setsockopt的SO_LINGER控制close的行為
#include<sys/socket.h>
struct linger
{
int l_onoff; // 關閉控制
int l_linger; // 控制時間
}
close可能會有三種行為:
- l_onoff:關閉時(值為0),close默認行為,發送緩沖區所有數據后關閉連接
- l_onoff:打開時(值大于0),若l_linger為0,close系統調用立即返回,緩沖區數據被丟棄,給對端發送RST報文
- l_onoff:打開時(值大于0),若l_linger大于0:
- 阻塞型socket,close等待l_linger的時間,直到發送完緩沖區數據并收到對端的ACK,如果這段時間沒有發送完緩沖區數據并收到確認,close將返回-1并設置errno為EWOULDBLOCK。
- 非阻塞型socket,立即返回,根據返回值和errno來判斷殘留數據是否發送完畢
shutdown
#include<sys/socket.h>
int shutdown(int sockfd,int howto);
不引用計數直接關閉,howto參數:
- SHUT_RD:程序不能再對socketfd做讀操作,接收緩沖區數據被丟掉
- SHUT_WR:關閉socketfd寫,緩沖區數據會在關閉前發送出去,寫操作不可執行(半關閉狀態)
- SHUT_RDWR:同時關閉
數據讀寫
除了默認對文件描述符的read、write操作之外,socket提供了專門的讀寫數據函數
TCP讀寫(recv & send)
#include<sys/socket.h>
// recv成功時返回讀取到的長度,實際長度可能小于len
// 發生錯誤返回-1設置errno,返回0表示連接關閉
ssize_t recv(int sockfd, void*buf, size_t len, int flags);
// 成功時返回寫入的數據的長度,失敗返回-1這是errno
ssize_t send(int sockfd, const void*buf, size_t len, int flags);
flags提供了一些選項設置:
- MSG_OOB(recv&send):發送或接收緊急數據,也叫帶外數據,在傳輸層的七七八八中首部信息中有說,在URG標志位1時該字段有效,seq + Urgen Pointer - 1的這一個字節是緊急數據(緊急數據只有一個字節),例如:
char buffer[1024];
memset(buffer, '\0', 1024);
// 發送端發送帶外數據hello
const char *oob_data = "hello";
send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
ret = recv(connfd, buffer, BUFESIZE - 1, 0);
// 接收到hell
ret = recv(connfd, buffer, BUFESIZE - 1, MSG_OOB); // 接收端接收帶外數據
// 接收到o
hell為正常數據,o為帶外數據,只有最后一個字節會被認為是帶外數據,前面的是正常數據。正常數據的接收會被帶外數據截斷。
-
int sockatmark(int sockfd);可以判斷下一個數據是不是帶外數據,1為是,此時可以利用MSG_OOB標志的recv調用來接收帶外數據。 - 通過SIGUSR信號觸發對帶外數據的處理
- MSG_DONTWAIT(recv&send):對socket的此次send或recv是非阻塞操作(相當于使用O_NONBLOCK)
- MSG_WAITALL(recv):一直讀取到請求的數據全部返回后recv函數返回
UDP讀寫(recvfrom & sendto)
通常這兩個函數用于無連接的套接字,如果用于有連接的讀寫可以把后兩位置為NULL
#include <sys/socket.h>
// 可以接收UDP,也可以接收TCP(后兩個參數置位NULL,因為TCP是面向連接的)
ssize_t recvfrom(int sockfd,void* buf,size_t len,int flags,
struct sockaddr* src_addr,socklen_t* addrlen);
// 可以接收UDP,也可以接收TCP(后兩個參數置位NULL,因為TCP是面向連接的)
ssize_t sendto(int sockfd,const void* buf,size_t len,int flags,
const struct sockaddr* dest_addr,socklen_t addrlen);
更高級的讀寫(recvmsg & sendmsg)
使用sendmsg可以將多個緩沖區的數據合并發送
使用recvmsg可以將接收的數據送入多個緩沖區,或者接收輔助數據
#include<sys/socket.h>
ssize_t recvmsg(int sockfd,struct msghdr* msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr* msg,int flags);
msghdr結構
struct msghdr
{
void* msg_name; // socket地址,如果是流數據,設置為NULL
socklen_t msg_namelen; // 地址長度
struct iovec* msg_iov; // I/O緩存區數組,分散的緩沖區
int msg_iovlen; // I/O緩存區數組元素數量
void* msg_control; // 輔助數據起始位置
socklen_t msg_controllen; // 輔助數據字節數
int msg_flags; // 等于recvmsg和sendmsg的flags參數,在調用過程中更新
};
輔助函數
獲取地址
#include<sys/socket.h>
// 獲取socketfd本端的地址信息,存到address,如果address長度大于address_len,將被截斷
int getsockname(int sockfd,struct sockaddr*address,socklen_t*address_len);
// 獲取socketfd遠端的地址信息
int getpeername(int sockfd,struct sockaddr*address,socklen_t*address_len);
成功返回0,失敗返回-1設置errno
socketfd屬性設置,option
#include<sys/socket.h>
int getsockopt(int sockfd,int level,int option_name,
void*option_value,socklen_t*restrict option_len);
int setsockopt(int sockfd,int level,int option_name,
const void*option_value,socklen_t option_len);
成功返回0,失敗返回-1設置errno,記錄一下option_name,后面用到結合具體實例分析
gethostbyname & gethostbyaddr
根據主機名稱獲取主機的完整信息、根據地址獲取主機的完整信息,信息返回結構如下:
#include<netdb.h>
struct hostent
{
char* h_name; /*主機名*/
char** h_aliases; /*主機別名列表,可能有多個*/
int h_addrtype; /*地址類型(地址族)*/
int h_length; /*地址長度*/
char** h_addr_list /*按網絡字節序列出的主機IP地址列表*/
};
getservbyname & getservbyport
根據服務名稱或端口號獲取服務信息,從/etc/services獲取信息,該文件中存放的是知名端口號和協議等信息。返回結構體如下:
#include<netdb.h>
struct servent
{
char* s_name; /*服務名稱*/
char** s_aliases; /*服務的別名列表,可能有多個*/
int s_port; /*端口號*/
char* s_proto; /*服務類型,通常是tcp或者udp*/
};
getaddrinfo
可以認為是調用了gethostbyname和getservbyname
#include<netdb.h>
// hostname:可以是主機名或IP地址字符串
// service:可以接收服務名,也可以接收十進制端口號
// result指向返回結果的鏈表,結構為addrinfo
int getaddrinfo(const char* hostname,const char* service,const
struct addrinfo* hints,struct addrinfo** result);
addrinfo結構體:
struct addrinfo
{
int ai_flags; /*大部分設置hints參數*/
int ai_family; /*地址族*/
int ai_socktype; /*服務類型,SOCK_STREAM或SOCK_DGRAM*/
int ai_protocol; /*通常設置為0*/
socklen_t ai_addrlen; /*socket地址ai_addr的長度*/
char* ai_canonname; /*主機的別名*/
struct sockaddr* ai_addr; /*指向socket地址*/
struct addrinfo* ai_next; /*指向下一個sockinfo結構的對象*/
};
getaddrinfo結束后,釋放result分配的堆內存
void freeaddrinfo(struct addrinfo* res);
getnameinfo
可以認為是調用了gethostbyaddr和getservbyport
#include<netdb.h>
// 返回的主機名存儲在host,服務名存儲在serv
int getnameinfo(const struct sockaddr *sockaddr,socklen_t addrlen,
char* host,socklen_t hostlen,char *serv,socklen_t servlen,int flags);
gai_strerror
轉換getnameinfo和getaddrinfo返回的錯誤碼為可讀的字符串
#include<netdb.h>
const char* gai_strerror(int error);
getaddrinfo和getnameinfo返回的錯誤碼如下:
簡單示例
testserver.cc,testserver 0.0.0.0 8889
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cassert>
#include <cstdio>
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
if (argc <= 2) {
cout << "usage:" << argv[0] << " ip_address port_number" << endl;
return 0;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address, client_addr;
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);
ret = listen(sockfd, 2);
assert(ret != -1);
socklen_t client_addr_length = sizeof(client_addr);
int conn =
accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_length);
if (conn < 0)
cout << "connect error: " << errno << endl;
else {
string hello = "hello client";
send(conn, hello.data(), sizeof(hello), 0);
close(conn);
}
close(sockfd);
return 0;
}
testclient.cc,/etc/hosts加入server的地址和主機名,testclient myserver
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cassert>
#include <iostream>
using namespace std;
int main(int argc, char* argv[]) {
if (argc != 2) {
cout << "usage: " << argv[0] << " hostname" << endl;
return 0;
}
char* hostname = argv[1];
// 獲取主機信息
struct hostent* hostinfo = gethostbyname(hostname);
assert(hostinfo);
/*
獲取server返回信息,自定義一個服務,
編輯/etc/services, my 8889/tcp
*/
struct servent* servinfo = getservbyname("my", "tcp");
assert(servinfo);
cout << "myserver port is " << ntohs(servinfo->s_port) << endl;
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int result = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
assert(result != -1);
char buffer[128];
result = recv(sockfd, buffer, sizeof(buffer), 0);
cout << "resceived: " << result << endl;
assert(result > 0);
buffer[result] = '\0';
cout << "server's message: " << buffer << endl;
close(sockfd);
return 0;
}
學習自:
《Linux高性能服務器編程》
《UNIX環境高級編程》
《UNIX系統編程》
總結
以上是生活随笔為你收集整理的Linux socket API的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GPTs Hunter 是什么?
- 下一篇: 个人编程助手: 训练你自己的编码助手