1. socket API
先理解主机字节序和网络字节序:
我们都知道不同的操作系统分为大端地址(高字节存低地址)和小端地址(高字节存高地址,低字节存低地址)。
小端字节序:主机字节序
大端字节序:网络字节序
区分大端和小端code:
void byteorder() {
union{
int value;
char union_bytes[sizeof(int)];
}test;
test.value = 0x0102;
if((test.union_bytes[0]==1) && (test.union_bytes[1]==2))
printf("big endian\n");
else
printf("little endian\n");
}
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);
htonl和htons 表示:host to net 转换, htonl 一般用来转换IP地址,htons一般用来转换端口号。ntohl和ntohs 相反。
sockaddr的结构体:
TCP IPv4:
#include <bits/socket.h>
struct sockaddr_in
{
sa_family_t sin_family; //地址族: AF_INET
u_int16_t sin_port; // 端口号,使用网络字节序
struct in_addr sin_addr; //IPV4地址结构体
};
sa_family_t 表述地址族类型:
我们经常看到PF和AF, 含义相同。
地址结构体:
struct in_addr
{
u_int32_t s_addr; //IPV4地址,要用网络字节序表示
};
TCP IPv6结构体:
struct sockaddr_in16
{
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_in; //scope ID
}
struct in6_addr
{
unsigned char sa_addr[16]; //IPV6地址,要用网络字节序表示
}
UDP的结构体:
#include <sys/un.h>
struct sockaddr_un
{
sa_family_t sin_family; //地址族:AF_UNIX
char sun_path[108]; //文件路径名
};
IP地址转换函数:
#include <arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_pton(int af, const char* src, void *dst);
char* inet_ntoa(struct in_addr in);
const char* inet_ntop(int af, const void * src, char* dst, socklen_t cnt);
inet_addr:将十进制字符串表示的IPV4地址转换为网络字节序整数表示的IPV4地址。
inet_pton:用字符串表示的src转换成用网络字节序表示的IP地址,并将转换结果存储在dst指向的内存中,af: 地址族(AF_INET),成功时返回1,失败返回0并设置errno。
inet_ntoa:将网络字节序转换为十进制表示的字符串。
inet_ntop: 和inet_pton进行相反的转换。cnt: 指定目标存储单元的大小。成功时返回目标单元的地址,失败返回MULL并设置errno。可以用下面的两个宏进行指定:
#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46
2. 创建socket
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:协议族,TCP 设置为AF_INET, UDP设置为PF_UNIX;
type:指服务类型。SOCK_STREAM 流服务, SOCK_UGRAM 数据报服务。
protol: 表示协议, 设置为0,让操作系统自己选择。
调用成功返回一个描述符,失败返回-1并设置errno;
3 bind
将一个socket与socket地址进行绑定。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen)
sockfd: 创建socket的fd
my_addr: sockaddr_in的结构体
addrlen: sizeof(struct sockaddr_in)的大小
4. listen
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd: 创建socket的fd
backlog: 是指所有处于半连接状态(三次握手 SYN_RCVD)和完全连接状态(ESTABLISHED)的socket 的上限。自内核2.2之后,它只表示处于完全连接状态的socket的上限,由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。backlog参数的典型值是5。
函数成功返回0, 失败则返回-1并设置errno.
5. accept
#include <sys/socket.h>
#include <sys/types.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen);
sockfd: 创建socket的fd.
addr: struct sockaddr_in 结构体地址
addrlen: sizeof(struct sockaddr_in)
accept成功时返回一个新的连接socket, 失败时返回-1
6. connect
客户端需要调用connect来主动与服务器建立连接。
#include <sys/socket.h>
#include <sys/types.h>
int connect(int sockfd, const struct sockaddr* serv_addr, socklen_t addrlen);
sockfd: 创建socket的fd.
serv_addr: 服务器监听的socket地址
addrlen: 指定这个地址的长度
7. close
关闭对应的socket
#include <unistd.h>
int close(int fd);
fd参数是待关闭的socket。 不过,close系统调用并非总是立即关闭一个连接,而是将fd的引用计数减1.只有当fd的引用计数为0时,才真正关闭连接。
如果无论如何都要立即终止连接(不是将socket的引用计数减1),可以使用如下的shutdown系统调用。
#include <sys/socket.h>
int shutdown(int sockfd, int howto)
sockfd 是待关闭的socket。 howto参数决定了shutdown的行为。值可取下表
8. 数据读写
TCP流数据的读写系统调用是:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
buf和len分别是读写缓冲区的大小和长度,flags通常设置为0. recv成功返回实际读取到的数据的长度,它可能小于我们的期望长度len。因此我们可能需要多次调用recv,才能读取到完整的数据。recv可能返回0,意味着通信的对方已经关闭连接了。recv出错时返回-1并设置errno。send成功时返回实际写入的数据的长度,失败则返回-1并设置errno。flags为数据提供了额外的控制,通常设为0.
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, struct sockaddr* dst_addr, socklen_t* addrlen);
因为UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址,即参数src_addr所指的内容,addrlen参数则指定该地址的长度。也可用于TCP,只需要把最后两个参数设置为NULL,因为TCP已经知道了对端的地址。
通用的数据读写:
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr* msg, int flags);
ssize_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; //分散的内存块
void *msg_control; //分散的内存块数量
socklen_t msg_controllen; //指向辅助数据的起始位置
int msg_flags; //复制函数中的flags参数, 并在调用过程中更新
};
struct iovec
{
void *iov_base; //内存的起始地址
size_t iov_len; //这块内存的长度
};
recvmsg数据被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和导航度则由msg_iov指向的数组指定,这称为分散读。sendmsg, msg_iovlen块分散内存中的数据一并发送,这称为集中写。
9. 带外标记
#include <sys/socket.h>
int sockatmark(int sockfd);
sockatmark判断sockfd是否处于带外标记,如果是,sockatmark返回1, 可以利用带MSG_OOB标志的recv调用来接收带外数据。如果不是,则sockatmark返回0。
10. socket选项
#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);
level指定要操作哪个协议的选项。option_value和option_len分别是被操作选项的值和长度。具体如下:
函数成功时返回0,失败时返回-1并设置errno。
这个需要注意几个参数比较重要:
SO_REUSEADDR
TCP连接的TIME_WAIT状态,服务器可以通过设置socket选项SO_REUSEADDR来强制使用被处于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_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));
SO_RCVBUF和SO_SNDBUF
这两个参数分别表示TCP的接收缓冲区和发送缓冲区的大小。但是,当我们用setsockopt来设置TCP的接收缓冲区和发送缓冲区的大小时,系统都会将其值加倍,并且不得小于某个最小值。TCP的接收缓冲区的最小值是256字节,而发送缓冲区最小值是2048个字节。这就是为什么需要多次接收的原因。系统这样做的目的,主要是确保一个TCP连接拥有足够的控线缓冲区来处理拥塞。我们可以修改内核参数/proc/sys/net/ipv4/tcp_rmem和/proc/sys/net/ipv4/tcp_wmem来强制修改。