Linux 高性能服务器网络编程
参考自《高性能服务器编程》,主要用于学习网络编程模块
Linux网络编程基础API
Socket 地址API
(1)网络字节序:大端字节序(低位存放高地址)
(2)主机字节序:小端字节序 (低位存放低地址)
通用socket 地址
#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协议族 |
专用Sokect地址
注:此处只学习IPV4/IPV6协议族,对UNIX本地族域不做解释
// 這些都不建議使用!請使用 inet_pton() 或 inet_ntop() 取代!
#include <sys/un.h>
struct sockaddr_in
{
sa_family_t sin_family; /*地址族: AF_UNIX*/
u_int16_t sin_port; /*端口号, 要用网络字节序*/
struct in_addr sin_addr; /*IPV4地址结构体*/
};
struct in_addr /*IPV4地址结构体*/
{
u_int32_t s_addr;
};
struct sockaddr_in6
{
sa_family_t sin6_family; /*地址族: AF_UNIX*/
u_int16_t sin6_port; /*端口号, 要用网络字节序*/
u_int32_t sin6_flowinfo; /*流信息,应该设置0*/
struct in6_addr sin_addr; /*IPV4地址结构体*/
u_int32_t sin6_scope_id;
};
struct in_addr /*IPV4地址结构体*/
{
unsigned char sa_addr[16]
};
IP地址转换函数
#include <arpa/inet.h>
/*description :点分十进制字符串表示的IPV4转换成网络字节序整数表示的IPv4地址
*return 成功 :网络字节序整数表示的IPv4地址 失败 :INADDR_NONE
*@pram strptr : 点分十进制字符串表示的IPV4
*/
in_addr_t inet_addr( const char *strptr);
/*description :点分十进制字符串表示的IPV4转换成网络字节序整数表示的IPv4地址
*return 成功 :1 失败 :0
*@pram cp : 点分十进制字符串表示的IPV4,inp:转换结果存于网络字节序结果
*/
int inet_aton( const char *cp, struct in_addr *inp);
/*description :网络字节序整数表示的IPv4地址转换成点分十进制字符串表示的IPV4
*return 成功 :成点分十进制字符串表示的IPV4 失败 NULL
*@pram in : 点分十进制字符串表示的IPV4
*/
注:此函数是不可重入函数,返回的值指向该静态内存
char *inet_ntoa( struct in_addr in);
*重点
#include <arpa/inet.h>
/*description :网络字节序转换为主机字节序
*return 成功 1 失败 0 并且设置 errno
*@pram af : AF_INET或者AF_INET6 src : 点分十进制字符串表示的IPV4或者点分十六进制字符串表示的IPV6
* dst : 转换结果存于网络字节序结果, size:INET_ADDRSTRLEN(16)/INET6_ADDRSTRLEN(46)
*/
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
/*description :主机字节序转换成网络字节序
*return 成功 1 失败 0 并且设置 errno
*@pram af : AF_INET或者AF_INET6 src : 点分十进制字符串表示的IPV4或者点分十六进制字符串表示的IPV6 dst : 转换结果存于网络字节序结果
*/
int inet_pton(int af, const char *src, void *dst);
创建socket(socket)
#include <sys/types.h>
#include <sys/socket.h>
/*description :创建socket文件
*return 成功:socket文件描述符 失败: 返回-1并且设置errno
*@pram domain(底层协议) : PF_INET / PF_INET6;
*type(服务类型) :SOCK_STREAM(TCP流服务)/ SOCK_DGRAM(UDP数据报) / SOCK_NONBLOCK(设置非阻塞) / SOCK_CLOEXEC(用fork创建子进程时在子进程中关闭该socket)
*protocol : 0
*/
int socket(int domain, int type, int protocol);
命名(绑定)socket(bind)
用于绑定具体的socket和port
#include <sys/types.h>
#include <sys/socket.h>
/*description :绑定socket,将my_addr所指的socket地址分配给为命名的sockfd 文件描述符,addrlen参数指出该socket地址的长度
*return 成功:0 失败: 返回-1并且设置errno
* 常见errno: EACCES : 普通用户绑定到0~1023端口上/EADDREINUSE:TIME_WAIT状态的socket地址
*@pram sockfd 文件描述符; my_addr : sockaddr; addrlen : 地址长度
*/
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
监听socket(listen)
/*description :监听指定socket文件描述符
*return 成功:0 失败: 返回-1并且设置errno
*@pram sockfd 文件描述符;
*backlog : 指定所有处于半连接状态(SYN_RCVD)和完全连接状态(ESTABLISHED)的socket的上限
*内核2.23之后,backlog只表示完全连接状态(ESTABLISHED)的上限,而半连接直接由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义
*backlog参数典型值为 5
*/
int listen(int sockfd, int backlog);
注:完整连接一般为(backlog + 1),不同的系统略有不同不过监听队列中的完整的连接的上限比bakclog略大
接收连接accpet
#include <sys/types.h>
#include <sys/socket.h>
/*description :从listen监听队列中接收一个连接
*return 成功:0 失败: 返回-1并且设置errno
*@pram sockfd : listen Socket文件描述符;addr : 获取接收连接远端的socket地址 addrlen:长度
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
注:accpet函数只是往监听队列取出连接,不管连接处于什么状态,更加不关心网络变化
发起连接(connect)
#include <sys/types.h>
#include <sys/socket.h>
/*description :客户端主动向服务器建立连接
*return 成功:0 失败: 返回-1并且设置errno
*ECONNREFUSED 目的端口不存在 ETIMEDOUT : 连接超时
*@pram sockfd : 返回与服务端通信的socket;serv_addr : 服务端地址 addrlen:长度
*/
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
关闭连接
#include <unistd.h>
int close( int fd);
注:并非总是关闭一个连接,而是将fd 的引用计数减1,只有当fd的引用计数为0时才真正的关闭连接,
多进程程序中一次fork会将父进程中打开的socket + 1,因此在父子进程中都要对该socket
执行close才能将连接关闭
/*description :关闭一方连接
*return 成功:0 失败: 返回-1并且设置errno
*@pram sockfd : 需要关闭的socket
*how :如下有详解
*/
int shutdown(int sockfd, int how)
how参数可选值
可选值 | 含义 |
---|---|
SHUT_RD(0) | 关闭sockfd的读操作,应用程序不能从sockfd执行读操作,并且把该socket接收缓冲区的数据丢弃 |
SHUT_WR (1) | 关闭sockfd的写操作,sockfd的发送缓冲区中的数据会在真正的关闭连接之前全部发出去,应用程序不在对sock执行写操作。这种情况连接处于半关闭状态 |
SHUT_RDWR(2) | 关闭sockfd的读写操作 |
读写操作
#include <sys/types.h>
#include <sys/socket.h>
/*description :从sockfd写数据
*return :实际读取的长度,可能小于期望值需要多次调用recv;出错时返回-1并且设置errno
*@pram buf : 读缓冲区的位置 len:读缓冲区的大小
*/
size_t send(int sockfd, const void *buf, size_t len, int flags);
/*description :从sockfd接收数据
*return :实际读取的长度,可能小于期望值需要多次调用recv;当返回 0 时对方关闭了连接, 出错时返回-1并且设置errno
*@pram buf : 读缓冲区的位置 len:读缓冲区的大小
*/
size_t recv(int sockfd, void *buf, size_t len, int flags);
一个基本的C/S程序
client
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <linux/in.h>
#include <signal.h>
extern void sig_proccess(int signo);
extern void sig_pipe(int signo);
static int s;
void sig_proccess_client(int signo)
{
printf("Catch a exit signal\n");
close(s);
exit(0);
}
#define PORT 8888 /* 侦听端口地址 */
int main(int argc, char *argv[])
{
struct sockaddr_in server_addr; /* 服务器地址结构 */
int err;/* 返回值 */
signal(SIGINT, sig_proccess);
signal(SIGPIPE, sig_pipe);
/* 建立一个流式套接字 */
s = socket(AF_INET, SOCK_STREAM, 0);
if(s < 0){/* 出错 */
printf("socket error\n");
return -1;
}
/* 设置服务器地址 */
bzero(&server_addr, sizeof(server_addr)); /* 清0 */
server_addr.sin_family = AF_INET; /* 协议族 */
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 本地地址 */
server_addr.sin_port = htons(PORT); /* 服务器端口 */
/* 将用户输入的字符串类型的IP地址转为整型 */
inet_pton(AF_INET, argv[1], &server_addr.sin_addr);
/* 连接服务器 */
connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
process_conn_client(s); /* 客户端处理过程 */
close(s); /* 关闭连接 */
}
server:
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <linux/in.h>
#include <signal.h>
extern void sig_proccess(int signo);
#define PORT 8888 /* 侦听端口地址 */
#define BACKLOG 2 /* 侦听队列长度 */
int main(int argc, char *argv[])
{
int ss,sc; /* ss为服务器的socket描述符,sc为客户端的socket描述符 */
struct sockaddr_in server_addr; /* 服务器地址结构 */
struct sockaddr_in client_addr; /* 客户端地址结构 */
int err; /* 返回值 */
pid_t pid; /* 分叉的进行id */
signal(SIGINT, sig_proccess);
signal(SIGPIPE, sig_proccess);
/* 建立一个流式套接字 */
ss = socket(AF_INET, SOCK_STREAM, 0);
if(ss < 0){/* 出错 */
printf("socket error\n");
return -1;
}
/* 设置服务器地址 */
bzero(&server_addr, sizeof(server_addr)); /* 清0 */
server_addr.sin_family = AF_INET; /* 协议族 */
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);/* 本地地址 */
server_addr.sin_port = htons(PORT); /* 服务器端口 */
/* 绑定地址结构到套接字描述符 */
err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));
if(err < 0){/* 出错 */
printf("bind error\n");
return -1;
}
/* 设置侦听 */
err = listen(ss, BACKLOG);
if(err < 0){/* 出错 */
printf("listen error\n");
return -1;
}
/* 主循环过程 */
for(;;) {
int addrlen = sizeof(struct sockaddr);
/* 接收客户端连接 */
sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen);
if(sc < 0){ /* 出错 */
continue; /* 结束本次循环 */
}
/* 建立一个新的进程处理到来的连接 */
pid = fork(); /* 分叉进程 */
if( pid == 0 ){ /* 子进程中 */
close(ss); /* 在子进程中关闭服务器的侦听 */
process_conn_server(sc);/* 处理连接 */
}else{
close(sc); /* 在父进程中关闭客户端的连接 */
}
}
}
地址信息函数
#include <sys/unistd.h>
/*description :获取hostName
*return :成功:0 失败: 返回-1并且设置errno
*@pram name:存储hostname len:缓冲区大小
*/
int gethostname(char *name, size_t len);
#include <sys/socket.h>
//获取本端的sock地址,将其存储在address参数指定内存中,socket长度存储在address——len参数指定变量中
//*return :成功:0 失败: 返回-1并且设置errno
int getsockname(int sockfd, struct sockaddr *address, socklen_t *address_len);
//获取本端的sock地址,将其存储在address参数指定内存中,socket长度存储在address——len参数指定变量中
//*return :成功:0 失败: 返回-1并且设置errno
int getpreename(int sockfd, struct sockaddr *address, socklen_t *address_len);
SOCKET选项(*)
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval,
socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname, const void *optval,
socklen_t optlen);
注:服务端对listen socket设置,accpet返回后会继承这些opt选项
客户端对connect设置,因为对于客户端connect结束就已经建立了连接
SO_REUSEADDR : 即使sock处于TIME_WAIT状态,只要设置了SO_RCVBUF也能立即重用
也可以修改内核参数:/proc/sys/net/ipv4/tcp_rw_recycle,快速回收关闭socket,从而tcp连接不进入TIME_WAIT的状态
SO_RECVBUF/SO_SNDBUF:用setsockopt设置时,系统一般会将设置的值加倍,但是需要在一定范围内,查看 /proc/sys/net/ipv4/tcp_wmem 和 /proc/sys/net/ipv4/tcp_rmem,目的是为了确保TCP连接有足够的空闲缓冲区来处理拥塞
SO_RCVLOWAT 和SO_SNDLOWAT选项:
作用:被IO复用系统调用(epoll)用来判断socket是否可读可写,当读缓冲区中可读数据大于读低水位时,到对应的socket读取数据;当写缓冲区中可读数据大于写低水位时,到对应的socket写入数据
SO_LINGER
作用:用于控制clsoe系统调用在关闭TCP连接时的行为。默认情况下,close调用之后立即将TCP发送缓冲区的数据发送给对端
#include <sys/socket.h>
struct linger
{
int l_onoff /*开启(非0)关闭(0)*/
int l_linger /*滞留时间*/
};
设置不同的变量在close之后会产生三种不同的行为:
(1)l_onoff = 0,默认关闭socket
(2)l_onoff != 0,调用close立即返回,丢弃发送缓冲区的数据,发送一个RST报文,因此这是一个终止异常连接的方法
(3)l_onoff != 0, l_linger > 0;close行为取决与缓冲区是否有数据,socket是否阻塞
对于阻塞socket,close等待一段时间,直到缓冲区数据发送并且确认,如果这段时间TCP模块没有发送完数据并且得到对方确认那么close系统调用返回-1,errno = EWOWLDBLOCK
对于非阻塞,close立即返回,根据返回值和errno的值来判断数据是否发送完毕
注:
#include <netdb.h>
//查看error的字符串形式
const char *gai_strerror( int error);
高级IO函数
pipe()
创建普通管道用于进程之间的通信
#include <unistd.h>
/*description :创建一个管道,实现进程间的通信
*return :成功:0 失败: 返回-1并且设置errno
*@pram name:存储hostname len:缓冲区大小
*/
int pipe( int fd[2]);
socket基础api提供了快速创建双向管道的
#include <sys/types.h>
#include <sys/socket.h>
/*description :创建一个管道,实现进程间的通信
*return :成功:0 失败: 返回-1并且设置errno
*@pram 与socket()参数相同,不过domain 只能用AF_UNIX
*/
int socketpair(int domain, int type, int protocol, int fd[2])
dup() / dup2()
#include <unistd.h>
/*description :创建一个新的文件描述符,指向file_descriptor相同的文件,管道或者网络连接
*return :成功:0 失败: 返回-1并且设置errno , 返回值总是取系统当前最小的文件描述符
*@pram
*/
int dup( int file_descriptor);
/*description :创建一个新的文件描述符,指向file_descriptor相同的文件,管道或者网络连接
*return :成功:0 失败: 返回-1并且设置errno , 返回值总是取不小于file_descriptor_two
*@pram
*/
int dup( int file_descriptor, int file_descriptor_two)
注:提供dup和dup2产生的新描述符并不继承原来文件描述符的属性,比如close-on-exec 和non-blocking
同时也是CGI服务器的基础
readv() / writev()
#include <sys/uio.h>
/*description :将数据从文件描述符读到分散的内存块即分散读/将分散内存的数据写入文件描述符,相当于简化的recvmsg/sendmsg
*return :成功:返回读出/写入的fd字节数 失败: 返回-1并且设置errno , 返回值总是取不小于file_descriptor_two
*@pram struct iovec
*{
* // Starting address (内存起始地址)
* void *iov_base;
* // Number of bytes to transfer(这块内存长度)
* size_t iov_len;
*}
*/
ssize_t readv( int fd, const struct iovec *vector, int count)
ssize_t writev( int fd, const struct iovec *vector, int count)
sendfile()
在两个文件描述符之间直接传递数据(有内核操作), 避免了内核与用户缓冲区的数据拷贝,sendfile( ) 系统调用利用 DMA 将文件中的数据拷贝到操作系统内核缓冲区中,然后数据被拷贝到与 socket 相关的内核缓冲区中。接下来,DMA 将数据从内核 socket 缓冲区中拷贝到网卡中去。如果在用户调用 sendfile ( ) 系统调用进行数据传输的过程中有其他进程截断了该文件,那么 sendfile ( ) 系统调用会简单地返回给用户应用程序中断前所传输的字节数,errno 会被设置为 success。
#include <sys/sendfile.h>
/*description :文件描述符之间传递数据
*return :成功:返回参数成功的字节数 失败: 返回-1并且设置errno , 返回值总是取系统当前最小的文件描述符
*@pram out_fd : 待写入内容,必须是个socket
* in_fd : 待读入内容,必须指向一个文件(支持mmap函数的文件描述符),不能是socket
* offset : 指定in_fd的那个位置开始读
* count : 参数的字节数
*/
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
mmap() / munmap()
用于申请一段共享内存(虚拟内存),可以作为IPC 的share memory 也可以文件之间映射到内存
#include <sys/mman.h>
/*description :申请一段内存空间
* return : 成功:返回指向目标区域的指针 失败:返回MAP_FAILED((void *)-1),并且设置errno
*@pram start : 指定内存的起始地址 length : 内存长度
* prot : 内存段的访问权限,如下
* PROT_READ : 内存端可读 PROT_WRITE : 内存段可写
* PROT_EXEC : 内存段可执行 PROT_NONE :内存段不可访问
*/
void *mmap( void *start, size_t length, int prot, int flags, int fd, off_t off_t);
/*description :释放一段内存空间
*return :成功:返回参数成功的字节数 失败: 返回-1并且设置errno
*@pram start : 指定内存的起始地址 length : 内存长度
*/
int munmap( void *start, size_t length);
flag参数:
splice()
用于两个文件描述符之间数据移动,也是零拷贝技术
#include <fcntl.h>
ssize_t splice(int fdin, loff_t *offin, int fdout, loff_t *offout, size_t len, unsigned int flags);
参数意义:
fdin参数:待读取数据的文件描述符。
offin参数:指示从输入数据的何处开始读取,为NULL表示从当前位置。如果fdin是一个管道描述符,则offin必须为NULL。
fdout参数:待写入数据的文件描述符。
offout参数:同offin,不过用于输出数据。
len参数:指定移动数据的长度。
flags参数:表示控制数据如何移动,可以为以下值的按位或:
SPLICE_F_MOVE:按整页内存移动数据,存在bug,自内核2.6.21后,实际上没有效果。
SPLICE_F_NONBLOCK:非阻塞splice操作,实际会受文件描述符本身阻塞状态影响。
SPLICE_F_MORE:提示内核:后续splice将调用更多数据。
SPLICE_F_GIFT:对splice没有效果。
fdin和fdout必须至少有一个是管道文件描述符。
返回值:
返回值>0:表示移动的字节数。
返回0:表示没有数据可以移动,如果从管道中读,表示管道中没有被写入数据。
返回-1;表示失败,并设置errno。
errno值如下:
EBADF:描述符有错。
EINVAL:目标文件不支持splice,或者目标文件以追加方式打开,或者两个文件描述符都不是管道描述符。
ENOMEM:内存不够。
ESPIPE:某个参数是管道描述符,但其偏移不是NULL。
fcntl() 重点
提供对文件描述符的各种控制操作,另外一个类似的系统调用ioctl,而且ioctl比fcntl执行的范围更加广
#include <fcntl.h>
int fcntl(int fd, int cmd, ...);
fd:被操作的文件描述符
cmd及其第三个定义如下图
一般网络编程需要将文件描述符设为非阻塞的
int setnonblocking( int fd )
{
int old_option = fcntl( fd, F_GETFL );
int new_option = old_option | O_NONBLOCK;
fcntl( fd, F_SETFL, new_option );
return old_option;
}