Linux网络编程基础API
5.1 socket地址API
现代CPU累加器一次可装早至少4字节,其在内存中的排序将影响它被累加器装载成的整数的值,即字节序问题。字节序分为大端字节和小端字节。大端字节序是指一个整数的高位字节存储在内存的低地址处,小端字节序则是指整数的高位字节存储在内存的高地址处。
// 判断机器字节序
#include <stdio.h>
void byteorder(){
union{
short value;
char union_bytes[sizeof(short)];
} test;
test.value = 0x0102;
if((test.union_bytes[0] == 1) &&
(test.union_bytes[0]==2)){
printf("big endian\n");
}
else{
printf("little endian\n");
}
}
现代PC大多采用小端字节序,因此小端字节序又被称为主机字节序。在两台不同字节序的主机之间传递数据时,发送端总是把要发送的数据转化成大端字节序数据在发送,因此大端字节序被称为网络字节序。
- Linux提供4个函数完成主机字节序和网络字节序之间的转换:
#include <netinet/in.h>
unsigned long int htonl( unsigned long int hostlong );
unsigned short int htons( unsigned short int hostshort );
unsigned long int ntohl( unsigned long int netlong );
unsigned short int ntohs( unsigned short int netshort );
5.1.2 通用socket地址
socket网络编程接口中表示socket地址的是接口提sockaddr,其定义为:
#include <bits/socket.h>
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
}
sa_family成员是地址族类型的变量,其与协议族类型对应,对应表为:
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPv4协议族 |
PF_INET6 | AF_INET6 | TCP/IPv6协议族 |
sa_data成员用于存放socket地址值,不同协议族的地址值具有不同的含义和长度
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX | 文件路径名,长度可达到108字节 |
PF_INET | 16 bit端口号和32 bit IPv4地址,共6字节 |
PF_INET6 | 16 bit端口号,32 bit流标识,128 bit IPv6地址,32 bit范围ID,共26字节 |
由于14字节的sa_data无法容纳多数协议族的地址值,因此Linux定义了新的通用socket地址结构体:
#include <bits/socket.h>
struct sockaddr_storage{
sa_family_t sa_family;
unsigned long int __ss_align;
char __ss_padding[128-sizeof(__ss_align)];
}
此结构体不仅提供足够大的空间用于存放地址值,且内存对齐(__ss_align的作用)。
5.1.3 专用socket地址
- UNIX本地协议族专用socket地址结构体:
#include <sys/un.h>
struct sockaddr_un{
sa_family_t sin_family; // 地址族:AF_UNIX
char sun_path[108]; // 文件路径名
};
- TCP/IP协议族有 sockaddr_in 和 sockaddr_in6 两个对应IPv4和IPv6的地质结构体:
struct sockaddr_in{
sa_family_t sin_family; // 地址族:AF_INET
u_int16_t sin_port; // 端口号:网络字节序表示
struct in_addr sin_addr; // IPv4地址结构体
};
struct in_addr{
u_int32_t s_addr; // IPv4地址,用网络字节序表示
};
struct sockaddr_in6{
sa_family_t sin6_family; // 地址族:AF_INET6
u_int16_t sin6_port; // 端口号
u_int32_t sin6_flowinfo; // 流信息,应设置为0
struct in6_addr sin6_addr; // IPv6地址结构体
u_int32_t sin6_scope_id; // scope ID,处于试验阶段
};
struct in6_addr{
unsigned char sa_addr[16]; // IPv6地址
};
所有专用socket地址类型的变量在实际使用时都需要转化为通用socket地址类型sockaddr(可使用强制转换),因此所有socked编程接口的地址参数类型都是sockaddr。
5.1.4 IP地址转换函数
通常人们用字符串表示IP地址,使用十六进制字符串表示IPv6地址。编程中将其转化为整数(二进制)方便使用。记录日志时将其转化成字符串。下面三个函数可用点分十进制字符串表示IPv4地址和用网络字节序正数表示的IPv4地=地址之间的转换:
#include <arpa/inet.h>
in_addr_t inet_addr( const char* strptr ); // 失败返回INADDR_NONE
int inet_aton( const char* cp, struct in_addr* inp ); // 成功返回1,失败返回0
char* inet_ntoa( struct in_addr in ); // 不可重入
下面更新的函数同样适用,并同时适用于IPv4地址和IPv6地址:
#include <arpa/inet.h>
int inet_pton( int af, const char* src, void* dst );
const char* inet_ntop( int af, const void* src, char* dst, socklen_t cnt );
// af参数指定地址族,inet_pton成功时返回1,失败返回0并设置errno
inet_ntop函数进行转换时,前三个参数与inet_pton相同,最后一个指定存储单元大小,可使用宏指定戴奥
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
// inet_ntop 成功时返回目标存储单元地址,失败时返回NULL并设置errno
5.2 创建socket
UNIX/Linux中,所有的东西都是文件,因此socket即一个可读、可写、可控制、可关闭的文件描述符。socket创建可通过socket系统调用:
#include <sys/types.h>
#include <sys/socket.h>
/**
* @brief 创建socket
* @param[in] domain 底层协议族类型(PF_INET/PF_INET6/PF_UNIX)
* @param[in] type 服务类型,主要包括SOCK_STERAM(流服务)和SOCK_UGRAM(数据报服务),通过选择服务类型确定使用TCP协议或UDP协议
* @param[in] protocol 选择一个具体协议,默认为0
* @return 调用成功返回socket文件描述符,失败返回-1并设置errno
**/
int socket( int domain, int type, int protocol );
5.3 命名socket
创建socket时,虽然给定了其地址族,但未指明该地址族具体的socket地址。将一个socket与socket地址绑定成为socket命名。客户都安一般不需要命名socket,而是采用匿名的方式。命名socket的系统调用是bind:
#include <sys/types.h>
#include <sys/socket.h>
/**
* @brief socket命名,将my_addr所指的socket地址分配给未命名的sockfd文件描述符
* @param[in] sockfd 未命名的文件描述符
* @param[in] my_addr 地址
* @param[in] addrlen 该socket地址的长度
* @return bind成功返回0, 失败则返回-1并设置errno
**/
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen);
// EACCES错误:被绑定的地址是受保护的地址
// EADDRINUSE错误:被绑定的地址正在使用中
5.4 监听socket
socket被明明后,需要使用系统调用创建一个监听队列,以存放待处理的客户连接:
#include <sys/socket.h>
/**
* @brief 创建一个监听队列
* @param[in] sockfd 指定被监听的socket
* @param[in] backlog 内核监听队列的最大长度
* @return 成功返回0, 失败则返回-1并设置errno
**/
int listen(int sockfd, int backlog);
实例:服务器程序,研究backlog参数对listen系统调用的实际影响
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
static bool stop = false;
// SIGTERM信号处理函数,出发时结束主程序内循环
static void handle_term(int sig){
stop = true;
}
int main(int argc, char* argv[]){
signal(SIGTERM, handle_term);
if(argc<=3){
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int backlog = atoi(argv[3]);
int sock = socket(PE_INET, SOCK_STREAM, 0);
assert(sock>=0);
//创建一个IPv4 socket地址
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, backlog);
assert(ret != -1);
while(!stop){
sleep(1);
}
// 关闭socket
close(sock);
return 0;
}
5.5 接受连接
#include <sys/types.h>
#include <sys/socket.h>
/**
* @brief 系统调用从listen监听队列中接受一个连接
* @param[in] sockfd 通过listen系统调用的监听的socket
* @param[in] addr 获取被接受链接的远端socket地址
* @param[in] addrlen 该socket地址的长度
* @return 成功返回一个新的连接socket,失败返回-1并设置errno
**/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
实例:服务器程序,接收一个异常的连接
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(int argc, char* argv[]){
if(argc<=2){
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
//创建一个IPv4 socket地址
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sock = socket(PE_INET, SOCK_STREAM, 0);
assert(sock>=0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, 5);
assert(ret != -1);
// 暂停20秒,以等待客户端连接或其他操作
sleep(20);
struct sockaddr_in client;
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd<0){
printf("errno is : &d\n", errno);
}
else{
//接受链接成功打印客户端的IP地址和端口号
char remote[INET_ADDRSTRLEN];
printf("connect with ip: %s and port: %d\n", inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRESTRLEN), ntohs(client.sin_port));
close(connfd);
}
// 关闭socket
close(sock);
return 0;
}
5.6 发起连接
客户端需要通过系统调用主动与服务器建立连接
#include <sys/tpyes.h>
#include <sys/socket.h>
/**
* @brief 客户端主动与服务器建立连接
* @param[in] sockfd 由socket调用返回一个socket
* @param[in] serv_addr 服务器监听的socket地址
* @param[in] addrlen 该socket地址的长度
* @return connect陈公公是返回0,失败返回-1并设置errno
**/
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
5.7 关闭连接
#include <unistd.h>
/**
* @brief 关闭连接
* @param[in] fd 待关闭的socket
**/
// close系统调用并非立即关闭一个连接,而是将fd减一
// 当fd == 0 时,才是真正的关闭连接
int close(int fd);
需要立即终止连接时:
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
// 成功返回1,失败返回-1并设置errno
howto参数选择:
可选值 | 含 义 |
---|---|
SHUT_RD | 关闭sockfd的读取,并将接收缓冲区数据丢弃 |
SHUT_WR | 关闭sockfd的写入,发发送缓冲区数据在关闭前全部发送出去 |
SHUT_RDWR | 同时关闭sockfd的读和写 |
5.8 数据读写
5.8.1 TCP数据读写
#include <sys/types.h>
#include <sys/socket.h>
// recv读取sockfd上的数据,buf和len指定缓冲区位置和大小
// 成功时返回得到的数据长度,若小于期望长度len,需要多次调用
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// send往sockfd上写数据,buf和len指定缓冲区的位置和大小
// 成功时返回实际写入的数据长度
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// flags提供了额外的控制
flags参数选取:
- MSG_CONFIRM:指定数据链路层协议持续坚挺对方的回应,知道得到答复(send:Y, recv:N);
- MSG_DONTROUTE:不查看路由表,直接将数据发送给本地局域网络内的主机(send:Y, recv:N);
- MSG_DONTWAIT: 对socket的本次操作僵尸非阻塞的(send:Y, recv:Y);
- MSG_MORE:告诉内核程序还有更多的数据要发送(send:Y, recv:N);
- MSG_WAITALL:读操作仅在读取到指定数量的字节后才返回(send:N, recv:Y);
- MSG_PEEK:窥探读缓存中的数据(send:N, recv:Y);
- MSG_OOB:发送或接收紧急数据(send:Y, recv:Y);
- MSG_NOSIGNAL:往读端关闭的管道或socket连接中写数据不引发SIGPIPE信号(send:Y, recv:N);
实例:发送带外数据
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(int argc, char* argv[]){
if(argc<=2){
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
//创建一个IPv4 socket地址
struct sockaddr_in server_address;
bzero(&address, sizeof(server_address));
server_address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &server_address.sin_addr);
server_address.sin_port = htons(port);
int sockfd = socket(PE_INET, SOCK_STREAM, 0);
assert(sock>=0);
if(connect(sockfd, (struct sockaddr*)&server_address,sizeof(server_address))<0){
printf("connection failed\n");
}
else{
const char* oob_data = "abc";
const char* normal_data = "123";
send(sockfd, normal_data, strlen(normal_data), 0);
send(sockfd, oob_data, strlen(obb_data), MSG_OOB);
send(sockfd, normal_data, strlen(normal_data), 0);
}
// 关闭socket
close(sock);
return 0;
}
实例:接收外带数据
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define BUF_SIZE 1024
int main(int argc, char* argv[]){
if(argc<=2){
printf("usage: %s ip_address port_number backlog\n", basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
//创建一个IPv4 socket地址
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int sockfd = socket(PE_INET, SOCK_STREAM, 0);
assert(sock>=0);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(sock, 5);
assert(ret != -1);
struct sockaddr_in client;
socklen_t client_addrlength = sizeof(client);
int connfd = accept(sock, (struct sockaddr*)&client, &client_addrlength);
if(connfd<0){
printf("errno is : %d\n", errno);
}
else{
char buffer[BUF_SIZE];
memset(buffer, '\0', BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE-1, 0);
printf("got %d bytes of normal data '%s'\n", ret, buffer);
memset(buffer, '\0', BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE-1, MSG_OOB);
printf("got %d bytes of normal data '%s'\n", ret, buffer);
memset(buffer, '\0', BUF_SIZE);
ret = recv(connfd, buffer, BUF_SIZE-1, 0);
printf("got %d bytes of normal data '%s'\n", ret, buffer);
close(connfd);
}
// 关闭socket
close(sock);
return 0;
}
5.8.2 UDP数据读写
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t len, int flags, const struct sockaddr* dest_addr, socklen_t addrlen);
5.8.3 通用数据读写函数
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
sszie_t sendmsg(int sockfd, struct msghdr* msg, int flags);
// msg参数是msghdr结构体类型的指针
struct msghdr{
void* msg_name; // socket地址
socklen_t msg_namelen; // socket地址的长度
struct iovec* msg_iov; // 分散的内存块
int msg_iovlen; // 分散内存块的数量
void* msg_control; // 指向辅助数据的起始位置
socklen_t msg_controllen; // 辅助数据的大小
int msg_flags; // 复制函数中的flags参数
};
struct iovec{
void *iov_base; // 内存起始地址
size_t iov_len; // 这块内存的长度
};
5.9 带外标记
由于在实际应用中,无法预期带外数据何时到来,因此Linux内核检测到TCP紧急标志时,将通知应用程序有带外数据需要接收。可通过IO复用产生的异常事件和SIGURG信号进行通知。系统调用中需要知道带外数据在数据流中的具体位置才能准确接受带外数据:
#include <sys/socket.h>
int sockatmark(int sockfd);
// 下一个被读取到的数据中若由带外数据,则返回1
// 即可通过recv中的MSG_OOB来接受带外数据
5.10 地址信息函数
#include <sys/socket.h>
// 获得本段sockfd的socket地址,并将其存储在address参数指定的内存中
int getsockname(int sockfd, struct sockaddr* address, socklen_t* address_len);
// 获得源端sockfd的socket地址
int getpeername(int sockfd, struct sockaddr* address, socklen_t* address_len);
5.11 socket选项
通过系统调用可专门用来读取和设置socket文件描述符属性:
#include <sys/socket.h>
// level指定操作那个协议的选项
// option_name指定选项的名字
// option_value、option_len是选项操作的值和长度
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
// 对服务器而言,有些socket选项只能在listen系统调用前针对socket设置才有效
// 对监听socket设置socket选项,则accept返回的链接socket将自动继承这些选项
5.11.1 SO_REUSEADDR选项
通过SO_RESUSEADDR选项可以强制使用被楚玉TIME_WAIT状态的链接占用的socket地址
int sock = socket(PF_INET, SOCK_STREAM, 0);
assert(sock>=0);
int reuse = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
struct sockaddr_int address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int ret = bind(sock, (struct sockaddr*)&address, sizeof(address));
5.11.2 SO_RCVBUF和SO_SNDBUF选项
SO_RCVBUF和SO_SNDBUF选项分别表示TCP接收缓冲区和发送缓冲区的大小,可确保TCP连接拥有足够的空闲缓冲区来处理拥塞。
5.11.3 SO_RCVLOWAT和SO_SNDLOWAT选项
SO_RCVLOWAT和SO_SNDLOWAT选项分别表示TCP接收缓冲区和发送缓冲区的低水位标记,其默认为1。一般被IO复用系统调用时判断socket是否刻度或可写。
5.11.4 SO_LINGER选项
SO_LINGER选项用于控制close系统调用在关闭TCP连接时的行为,当设置SO_LINGER值时,会将setsockopt系统调用传递给linger结构体
#include <sys/socket.h>
struct linger{
int l_onoff; // 开启或关闭该选项
int l_linger; // 留置时间
};
- l_onoff == 0:该选项不起作用。
- l_onoff != 0, l_linger == 0:close系统调用立即返回,TCP模块丢弃被关闭的socket对应的TCP缓冲区残留数据,并给对方发送一个复位报文段。此方法给服务器提供了异常终止的连接方法。
- l_onoff != 0, l_linger>0:阻塞的socket,close等待l_linger的时间,知道TCP模块发送完所有的残留数据并得到对方确认,若未得到返回-1并设置errno;非阻塞的socket,close立即返回,根据返回值和errno怕段擦流数据是否已经发送完毕。
5.12 网络API
5.12.1 gethostbyname和gethostbyaddr
gethostbyname函数根据主机名称获取主机的完整信息,gethostbyaddr根据IP地址获取主机的完整信息:
#include <netdb.h>
struct hostent* gethostbyname(const char* name);
struct hostent* gethostbyaddr(const void* addr, size_t len, int type);
struct hostent{
char* h_name; // 主机名
char** h_aliases; // 主机别名列表
int h_addrtype; // 地址类型
int h_length; // 地址长度
char** h_addr_list; // 按照网络字节序列出的主机IP地址列表
};
5.12.2 getservbyname和getservbyport
getservbyname根据名称获取某个服务的完整信息,getservbyport根据端口号获取某个服务的完整信息:
#include <netdb.h>
// proto:指定服务类型,"tcp" 或 "udp"
struct servent* getservbyname(const char* name, const char* proto);
struct servent* getservbyport(int port, const char* proto);
struct servent{
char* s_name; // 服务名
char** s_aliases; // 服务的别名列表
int s_port; // 端口号
char* s_proto; // 服务类型:tcp、udp
};
实例:访问daytime服务
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
int main(int argc, char *argv[]){
assert(argc == 2);
char *host = argv[1];
// 获取目标主机地址信息
struct hostent* hostinfo = gethostbyname(host);
assert(hostinfo);
// 获取daytime服务信息
struct servent* servinfo = getservbyname("daytime", "tcp");
assert(servinfo);
printf("daytime port is %d\n" ntohs(servinfo->s_port));
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 = read(sockfd, buffer, sizeof(buffer));
assert(result>0);
buffer[result] = '\0';
printf("the daytime is: %s\n", buffer);
close(sockfd);
return 0;
}
5.12.3 getaddrinfo
getaddrinfo可以通过主机并获得IP地址,也可以通过服务名获得端口号:
#include <netdb.h>
int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo** result);
struct addrinfo{
int ai_flags;
int ai_family; // 地址族
int ai_socktype; // 服务类型
int ai_protocol;
socklen_t ai_addrlen; // socket地址长度
char* ai_canonname; // 主机别名
struct sockaddr* ai_addr; // 指向socket地址
struct addrinfo* ai_next; // 直向下一个对象
};
// 需要配对函数来释放内存
void freeaddrinfo(struct addrinfo* res);
5.12.4 getnameinfo
可通过socket地址同时获得以字符串表示的主机名和服务名
#include <netdb.h>
itn getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen, char* host, socklent_t hostlen, char* serv, socklen_t servlen, int flags);
// 若出错,可利用下方函数将其转化成字符串
const char* gai_strerror(int error);