Linux socket API
socket是進程通信機制的一種,與PIPE、FIFO不同的是,socket即可以在同一臺主機通信(unix domain),也可以通過網(wǎng)絡在不同主機上的進程間通信(如:ipv4、ipv6),例如因特網(wǎng),應用層通過調(diào)用socket API來與內(nèi)核TCP/IP協(xié)議棧的通信,通過網(wǎng)絡字節(jié)實現(xiàn)不用主機之間的數(shù)據(jù)傳輸。
前置條件
字節(jié)序
對于多字節(jié)的數(shù)據(jù),不同處理器存儲字節(jié)的順序稱為字節(jié)序,主要有大端序(big-endian)和小端序(little-endian),字節(jié)序的收發(fā)不統(tǒng)一就會導致值被解析錯誤。
大端序
高位字節(jié)存低位內(nèi)存
大端序是最高位字節(jié)存儲在最低位內(nèi)存地址處。例如一段數(shù)據(jù)0x0A0B0C0D,0x0A是最高位字節(jié),0x0D是最地位字節(jié),內(nèi)存地址最低位a、最高位a+3,在大端序中存儲方式如下
- 8bit存儲方式:內(nèi)存地址從低到高0x0A -> 0x0B -> 0x0C -> 0x0D
- 16bit存儲方式:內(nèi)存地址從低到高0x0A0B -> 0x0C0D
小端序
低位字節(jié)存低位內(nèi)存
小端序是最低位字節(jié)存儲在最低位內(nèi)存地址處。例如一段數(shù)據(jù)0x0A0B0C0D,0x0A是最高位字節(jié),0x0D是最地位字節(jié),內(nèi)存地址最低位a、最高位a+3,在小端序存儲方式如下
- 8bit存儲方式:內(nèi)存地址從低到高0x0D->0X0C->0X0B->0X0A
- 16bit存儲方式:內(nèi)存地址從低到高0X0C0D->0X0A0B
主機通常使用小端序,因為計算機先處理小端序的字節(jié)效率更高。通過上面的結(jié)構不難看出,大端序更易讀,所以網(wǎng)絡和存儲等采用了大端序,那么網(wǎng)絡通信的時候就需要將網(wǎng)絡字節(jié)的大端序轉(zhuǎn)換為主機字節(jié)的小端序。好在這些都有系統(tǒng)調(diào)用可以保證~
判斷主機的字節(jié)序:
#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(); }
字節(jié)序轉(zhuǎn)換
#include<netinet/in.h>
// long型主機字節(jié)序轉(zhuǎn)換為long型網(wǎng)絡字節(jié)序, host to network
unsigned long int htonl(unsigned long int hostlong);
// short型
unsigned short int htons(unsigned short int hostshort);
// long型網(wǎng)絡字節(jié)序轉(zhuǎn)換為long型主機字節(jié)序, network to host
unsigned long int ntohl(unsigned long int netlong);
// short型
unsigned short int ntohs(unsigned short int netshort);
比方轉(zhuǎn)換主機的端口
int main(int argc, char *argv[]){
int port = atoi(argv[1]); // 主機序
server_address.sin_port = htons(port); // 網(wǎng)絡序
}
地址
通用地址
地址我們標識通信的端點,通用的地址格式為
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family; // 協(xié)議類型,例如 ipv4 AF_INET、unix AF_UNIX
char sa_data[14]; // unix域存放文件路徑,ip域存放ip地址和端口號
}
sa_data只能容納14字節(jié)地址數(shù)據(jù),如果是unix域路徑長度可以達到108字節(jié)放不下,所以linux定義了新的地址
#include<bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int__ss_align; // 作用是內(nèi)存對齊
char__ss_padding[128-sizeof(__ss_align)];
}
專有地址
專有地址在bind、accept、connect等需要用到的函數(shù)中需要強制轉(zhuǎn)換為通用地址,例如:(struct sockaddr *)&server_address
顧名思義專門為ipv4、unix、ipv6設計的不同socket地址結(jié)構,以ipv4為例
struct sockaddr_in
{
sa_family_t sin_family; // AF_INET
u_int16_t sin_port; // 網(wǎng)絡字節(jié)序的端口號
struct in_addr sin_addr; // IP地址
};
struct in_addr
{
u_int32_t s_addr; // 網(wǎng)絡字節(jié)序的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字符串轉(zhuǎn)換為網(wǎng)絡字節(jié)序整形表示的ip地址,存入sin_addr
inet_pton(AF_INET, ip, &address.sin_addr);
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 創(chuàng)建socket
// 綁定端口,要強制轉(zhuǎn)換為通用地址 (struct sockaddr *)&address
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
}
創(chuàng)建連接
創(chuàng)建socket
Linux一切皆文件,所以socket創(chuàng)建好之后就是一個文件描述符,對該fd讀寫關閉、屬性控制。
以ipv4為例
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- 第一個參數(shù)domain指定協(xié)議族,AF_INET、AF_UNIX、AF_INET6
- 第二個參數(shù)type指定socket類型,TCP\UDP分別使用流式SOCK_STREAM和數(shù)據(jù)報式SOCK_DGRAM
- 第三個參數(shù)protocal指定協(xié)議,有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連接的端口和地址
監(jiān)聽socket:listen
開始監(jiān)聽,并指定連接數(shù)
#include<sys/socket.h>
int listen(int sockfd,int backlog);
ret = listen(sock, 5);
- backlog參數(shù)表示處于ESTABLISHED狀態(tài)的連接數(shù)(我的ubuntu20.4測試為backlog+1),超過該值客戶端收到ECONNREFUSED或者客戶端TIMEOUT
接受連接:accept
從listen隊列中拿連接過來,不管該連接是ESTABLISED還是CLOSE_WAIT的狀態(tài)。
int connfd = accept(sockfd, (struct sockaddr *)&client, &client_addrlength);
發(fā)起連接:connect
connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address))
成功返回0,失敗返回-1并設置errno
- ECONNREFUSED:目標端口不存在或連接被拒絕
- ETIMEOUT:連接超時
關閉連接
close
關閉socket fd,默認情況下:如果是多進程,fork后會將fd引用計數(shù)加1,如果要關閉該socket,父子進程都需要close,而且是同時關閉讀和寫。可以通過setsockopt的SO_LINGER控制close的行為
#include<sys/socket.h>
struct linger
{
int l_onoff; // 關閉控制
int l_linger; // 控制時間
}
close可能會有三種行為:
- l_onoff:關閉時(值為0),close默認行為,發(fā)送緩沖區(qū)所有數(shù)據(jù)后關閉連接
- l_onoff:打開時(值大于0),若l_linger為0,close系統(tǒng)調(diào)用立即返回,緩沖區(qū)數(shù)據(jù)被丟棄,給對端發(fā)送RST報文
- l_onoff:打開時(值大于0),若l_linger大于0:
- 阻塞型socket,close等待l_linger的時間,直到發(fā)送完緩沖區(qū)數(shù)據(jù)并收到對端的ACK,如果這段時間沒有發(fā)送完緩沖區(qū)數(shù)據(jù)并收到確認,close將返回-1并設置errno為EWOULDBLOCK。
- 非阻塞型socket,立即返回,根據(jù)返回值和errno來判斷殘留數(shù)據(jù)是否發(fā)送完畢
shutdown
#include<sys/socket.h>
int shutdown(int sockfd,int howto);
不引用計數(shù)直接關閉,howto參數(shù):
- SHUT_RD:程序不能再對socketfd做讀操作,接收緩沖區(qū)數(shù)據(jù)被丟掉
- SHUT_WR:關閉socketfd寫,緩沖區(qū)數(shù)據(jù)會在關閉前發(fā)送出去,寫操作不可執(zhí)行(半關閉狀態(tài))
- SHUT_RDWR:同時關閉
數(shù)據(jù)讀寫
除了默認對文件描述符的read、write操作之外,socket提供了專門的讀寫數(shù)據(jù)函數(shù)
TCP讀寫(recv & send)
#include<sys/socket.h>
// recv成功時返回讀取到的長度,實際長度可能小于len
// 發(fā)生錯誤返回-1設置errno,返回0表示連接關閉
ssize_t recv(int sockfd, void*buf, size_t len, int flags);
// 成功時返回寫入的數(shù)據(jù)的長度,失敗返回-1這是errno
ssize_t send(int sockfd, const void*buf, size_t len, int flags);
flags提供了一些選項設置:
- MSG_OOB(recv&send):發(fā)送或接收緊急數(shù)據(jù),也叫帶外數(shù)據(jù),在傳輸層的七七八八中首部信息中有說,在URG標志位1時該字段有效,seq + Urgen Pointer - 1的這一個字節(jié)是緊急數(shù)據(jù)(緊急數(shù)據(jù)只有一個字節(jié)),例如:
char buffer[1024];
memset(buffer, '\0', 1024);
// 發(fā)送端發(fā)送帶外數(shù)據(jù)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); // 接收端接收帶外數(shù)據(jù)
// 接收到o
hell為正常數(shù)據(jù),o為帶外數(shù)據(jù),只有最后一個字節(jié)會被認為是帶外數(shù)據(jù),前面的是正常數(shù)據(jù)。正常數(shù)據(jù)的接收會被帶外數(shù)據(jù)截斷。
-
int sockatmark(int sockfd);可以判斷下一個數(shù)據(jù)是不是帶外數(shù)據(jù),1為是,此時可以利用MSG_OOB標志的recv調(diào)用來接收帶外數(shù)據(jù)。 - 通過SIGUSR信號觸發(fā)對帶外數(shù)據(jù)的處理
- MSG_DONTWAIT(recv&send):對socket的此次send或recv是非阻塞操作(相當于使用O_NONBLOCK)
- MSG_WAITALL(recv):一直讀取到請求的數(shù)據(jù)全部返回后recv函數(shù)返回
UDP讀寫(recvfrom & sendto)
通常這兩個函數(shù)用于無連接的套接字,如果用于有連接的讀寫可以把后兩位置為NULL
#include <sys/socket.h>
// 可以接收UDP,也可以接收TCP(后兩個參數(shù)置位NULL,因為TCP是面向連接的)
ssize_t recvfrom(int sockfd,void* buf,size_t len,int flags,
struct sockaddr* src_addr,socklen_t* addrlen);
// 可以接收UDP,也可以接收TCP(后兩個參數(shù)置位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可以將多個緩沖區(qū)的數(shù)據(jù)合并發(fā)送
使用recvmsg可以將接收的數(shù)據(jù)送入多個緩沖區(qū),或者接收輔助數(shù)據(jù)
#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結(jié)構
struct msghdr
{
void* msg_name; // socket地址,如果是流數(shù)據(jù),設置為NULL
socklen_t msg_namelen; // 地址長度
struct iovec* msg_iov; // I/O緩存區(qū)數(shù)組,分散的緩沖區(qū)
int msg_iovlen; // I/O緩存區(qū)數(shù)組元素數(shù)量
void* msg_control; // 輔助數(shù)據(jù)起始位置
socklen_t msg_controllen; // 輔助數(shù)據(jù)字節(jié)數(shù)
int msg_flags; // 等于recvmsg和sendmsg的flags參數(shù),在調(diào)用過程中更新
};
輔助函數(shù)
獲取地址
#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,后面用到結(jié)合具體實例分析
gethostbyname & gethostbyaddr
根據(jù)主機名稱獲取主機的完整信息、根據(jù)地址獲取主機的完整信息,信息返回結(jié)構如下:
#include<netdb.h>
struct hostent
{
char* h_name; /*主機名*/
char** h_aliases; /*主機別名列表,可能有多個*/
int h_addrtype; /*地址類型(地址族)*/
int h_length; /*地址長度*/
char** h_addr_list /*按網(wǎng)絡字節(jié)序列出的主機IP地址列表*/
};
getservbyname & getservbyport
根據(jù)服務名稱或端口號獲取服務信息,從/etc/services獲取信息,該文件中存放的是知名端口號和協(xié)議等信息。返回結(jié)構體如下:
#include<netdb.h>
struct servent
{
char* s_name; /*服務名稱*/
char** s_aliases; /*服務的別名列表,可能有多個*/
int s_port; /*端口號*/
char* s_proto; /*服務類型,通常是tcp或者udp*/
};
getaddrinfo
可以認為是調(diào)用了gethostbyname和getservbyname
#include<netdb.h>
// hostname:可以是主機名或IP地址字符串
// service:可以接收服務名,也可以接收十進制端口號
// result指向返回結(jié)果的鏈表,結(jié)構為addrinfo
int getaddrinfo(const char* hostname,const char* service,const
struct addrinfo* hints,struct addrinfo** result);
addrinfo結(jié)構體:
struct addrinfo
{
int ai_flags; /*大部分設置hints參數(shù)*/
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結(jié)構的對象*/
};
getaddrinfo結(jié)束后,釋放result分配的堆內(nèi)存
void freeaddrinfo(struct addrinfo* res);
getnameinfo
可以認為是調(diào)用了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
轉(zhuǎn)換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環(huán)境高級編程》
《UNIX系統(tǒng)編程》
總結(jié)
以上是生活随笔為你收集整理的Linux socket API的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GPTs Hunter 是什么?
- 下一篇: Linux下redis的安装下载以及连接