服务器开发-网络通信API和注意事项

Linux下网络编程常用函数

需要包含头文件

1
2
#include <sys/types.h>
#include <sys/socket.h>

socket

socket函数建立协议簇为aomain 协议类型为type 协议类型的某个特定类型为protocol的套接字文件描述符
成功返回标识套接字的文件描述符 失败返回-1,错误原因通过查看errno获得
常用协议簇 domain

  1. AF_UNIX 本机通信
  2. AF_INET IPV4
  3. AF_INET6 IPV6

常用协议类型 type

  1. SOCK_STREAM TCP套接字
  2. SOCK_DGRAM UDP套接字
  3. SOCK_RAM 原始套接字

协议类型的特定类型 protocol 一般置为0即可

1
int socket(int domain, int type, int protocol);

fcntl

fcntl函数针对文件描述符提供各种操作控制以改变已打开文件的各种属性
网络编程中主要使用其设置为阻塞和设置为非阻塞功能

1
2
3
4
5
6
7
8
9
10
11
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, long arg);

//设置为非阻塞
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

//设置为阻塞
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags & ~O_NONBLOCK);

getsockopt和setsockopt

getsockopt

getsockopt()函数用于获取任意类型、任意状态套接口的选项当前值,并把结果存入optval
sockfd 套接字文件描述符
level 选项定义的层次
optname 需获取的套接字选项
optval 指向存放所得选项值的缓冲区
optlen 指向optval缓冲区的长度值

1
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);

setsockopt

setsockopt()函数用于任意类型、任意状态套接口的设置选项值。
sockfd 套接字文件描述符
level 选项定义的层次
optname 需获取的套接字选项
optval 指向存放所得选项值的缓冲区
optlen 指向optval缓冲区的长度值

1
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

常用level和选项

level级别 optname选项名 选项值数据类型 适用函数 说明
SOL_SOCKET SO_ERROR int getsockopt 获取并清除套接字错误
SOL_SOCKET SO_REUSEADDR int setsockopt
SOL_SOCKET SO_REUSEPORT int setsockopt
SOL_SOCKET SO_KEEPALIVE int setsockopt
SOL_SOCKET SO_LINGER struct linger setsockopt
IPPROTO_TCP TCP_DEFER_ACCEPT int setsockopt
IPPROTO_TCP TCP_NODELAY int setsockopt 禁用Nagle算法

bind

bind函数绑定套接字和地址, 成功返回0, 失败返回-1, 错误原因可以通过errno获得 地址为32位的IPv4地址或128位的IPv6地址与16位的TCP或UDP端口的组合
sockfd 套接字文件描述符 通过socket函数获得
addr 指向特定协议的地址结构的指针 绑定0.0.0.0则绑定该机器上任意网卡地址,绑定127.0.0.1则只会绑定本地回环地址127.0.0.1
addrlen 该地址结构的长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

//示例
struct sockaddr_in addr;
/*设置sockaddr结构*/
addr.sin_family=AF_INET;
addr.sin_port=htons(SERVPORT);
addr.sin_addr.s_addr=INADDR_ANY;
bzero(&(addr.sin_zero), 8);
bind(sockfd,(struct sockaddr*)&addr, sizeof(struct sockaddr))

listen

listen监听函数,将一个套接字转化为监听套接字
sockfd 套接字文件描述符
backlog 连接建立完成的队列的长度

1
int listen(int sockfd, int backlog);

accept

accept接受连接 成功则返回建立了连接的套接字文件描述符 失败返回-1 错误原因通过查看errno获得
当监听的sockfd被设置为非阻塞而errno为EWOULDBLOCKECONNABORTEDEPROTOEINTR时需要忽略错误
sockfd 套接字文件描述符
addr 用来保存发起连接的主机的IP和端口号
addrlen addr指向的长度

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

connect

connect用于主动建立一个连接 成功返回0 失败返回-1 错误原因可以通过errno获得
当发起连接的sockfd被设置为非阻塞而errno为EINPROGRESS表示连接操作正在进行中,但是仍未完成.然后将sockfd的加入到I/O复用中,监听sockfd是否可读或者可写,如果只可写说明连接成功,如果即可读又可写分为两种情况:

  1. sockfd连接错误
  2. sockfd连接成功,socket读缓冲区得到了远程主机发送的数据

通过getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (void*)&e, &elen)获取sockfd错误信息 如果e为0表示连接成功 否则连接失败
sockfd 套接字文件描述符
addr 指向特定协议的地址结构的指针
addrlen 该地址结构的长度

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

如何将一个sockfd设为非阻塞

  1. 调用socket函数时设置

    1
    int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
  2. accept改为调用accept4设置最后的标记位为SOCK_NONBLOCK

    1
    2
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 
    int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
  3. 调用fcntl或ioctl函数(常用fcntl)

    1
    2
    fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK);
    ioctl(sockfd, FIONBIO, 1); //1:非阻塞 0:阻塞

connect非阻塞代码编写

connect能立即连接成功,则返回0;如果连接操作正在进行中或连接出错,则返回-1,连接操作正在进行中可通过errnoEINPROGRESSEINTR(中断)判断.然后将该sockfd放入I/O复用中进行,监听sockfd可读

非阻塞socket正确收发数据

非阻塞情况下recv数据

  1. 返回值大于0表示接收数据的大小
  2. 返回值等于0表示对端关闭连接
  3. 返回值为-1,errnoEWOULDBLOCKEINTR时接收完毕

在epoll的ET(边缘触发)模式下,一定要循环收取数据,直到收取干净为止.

非阻塞情况下send数据

  1. 返回值大于0表示发送数据的大小
  2. 返回值等于0表示对端关闭连接
  3. 返回值为-1,errnoEWOULDBLOCK表示TCP窗口容量不足,errnoEINTR表示中断,继续发送数据

丢包/粘包/包不完整

丢包:TCP是可靠的,不会丢包,也不存在包顺序错乱的问题
粘包:收取一个固定大小的包头信息,根据包头里面指定的包体大小收取包体大小
包不完整:循环接受数据,当发现包头或包体大小不够,数目数据不完整,继续等待新的数据到来

Nagle算法

nagle是操作系统网络通信层的一种发送数据包的机制.开启nagle算法后,一次放入网卡缓冲区的数据较小时,可能不会立即发出,只有当多次send或write之后,网卡缓冲区数据足够多时,才一次性发出.操作系统利用nagle算法减少网络通信次数,提供网络利用率,对于实时性要求比较高的应用,可以禁用nagle算法.

1
2
long noDelay = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY,(LPSTR)&noDelay, sizeof(long)); //noDelay为1禁用nagle算法,为0启用nagle算法

SO_REUSEADDR和SO_REUSEPORT

套接字由相关五元组组成:协议、本地地址、本地端口、远程地址、远程端口
解决一个socket被系统回收以后,在一个最大存活期(MSL,大约2分钟)内,该socket绑定的地址和端口号不能被重复利用的情况

心跳包

TCP协议的keepalive机制(开启SO_KEEPALIVE选项)
应用层心跳:

  1. 定时发送心跳包(只在两端没有数据往来的N秒后才需要发送,减轻服务器压力,减少网络通信流量)
  2. 由客户端发给服务端

重连

错误码EINTR

Linux网络函数(connect/send/recv/epoll_wait等)在出错时一定要检查错误是不是EINTR,如果是EINTR,操作其实只是被信号中断了,函数调用并没有出错.

减少系统调用

系统调用伴随着从用户空间到内核空间的切换.

忽略SIGPIPE信息

对一个已经收到FIN包的socket调用read方法,如果接收缓冲已空,则返回0,即连接已关闭.对一个已经收到FIN包的socket第一次调用write方法时,如果发送缓冲没问题,则write调用会返回写入的数据量,同时进行数据发送.但是发送出去的报文会导致对端发回RST报文.因为对端的socket已经调用了close进行了完全关闭,已经处于既不发送,也不接收数据的状态.所以第二次调用write方法时(假设在收到RST之后)会生成SIGPIPE信号,导致进程退出.
通俗点:对一个对端已经关闭的socket调用两次write,第二次将生成SIGPIPE信号.在写管道时,读进程已经终止,也将产生SIGPIPE信号

1
signal(SIGPIPE, SIG_IGN);       //屏蔽SIGPIPE信号后,第二次调用write方法时,会返回-1,并设置errno为EPIPE.