Linux网络编程笔记

概述

Linux网络编程是C/C++开发必须掌握的技能。

常用网络模型

select模型

(暂缺)

epoll模型

epoll支持水平触发和边缘触发,理论上来说边缘触发性能更高,但是使用更加复杂,因为任何意外的丢失事件都会造成请求处理错误。Nginx就使用了epoll的边缘触发模型。

这里提一下水平触发和边缘触发就绪通知的区别,这两个词来源于计算机硬件设计。它们的区别是只要句柄满足某种状态,水平触发就会发出通知;而只有当句柄状态改变时,边缘触发才会发出通知。例如一个socket经过长时间等待后接收到一段100k的数据,两种触发方式都会向程序发出就绪通知。假设程序从这个socket中读取了50k数据,并再次调用监听函数,水平触发依然会发出就绪通知,而边缘触发会因为socket“有数据可读”这个状态没有发生变化而不发出通知且陷入长时间的等待。

因此在使用边缘触发的 api 时,要注意每次都要读到 socket返回 EWOULDBLOCK为止。 否则netstat 的recv-q会持续增加。

通常来说,et方式是比较危险的方式,如果要使用et方式,那么,应用程序应该 1、将socket设置为non-blocking方式 2、epoll_wait收到event后,read或write需要读到没有数据为止,write需要写到没有数据为止(对于non-blocking socket来说,EAGAIN通常是无数据可读,无数据可写的返回状态).

epoll_create

int epoll_create(int size);

创建epoll实例,返回的epoll句柄是文件描述符,用完后需要close来关闭。

size: 用来告诉内核要监听文件描述符的数量。自Linux 2.6.8之后,这个参数被忽略,但必须设置为大于0的值。

epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

用来操作epoll实例,具体操作由op参数指定。struct epoll_event 的定义见下文。

epfd: epoll句柄。

op: 操作选项,有效操作选项如下:

  • EPOLL_CTL_ADD:注册新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL:从epfd中删除一个fd;

fd: 需要进行操作的目标文件描述符。

event: 描述与fd相关联的信息,struct epoll_event 的定义见下文。

epoll_wait

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

该函数用于等待事件的发生,struct epoll_event 的定义见下文。

epfd: epoll句柄。

events: 用来从内核得到事件的集合;

maxevents: 每次最多能处理的事件数;

timeout: 指定事件最小等待的时间,单位毫秒。

struct epoll_event

定义如下:

events: 可设置如下一个或多个事件。

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

主要函数

socket(创建socket文件描述符)

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

domain:指定协议族(AF_UNIX和AF_INET等),AF_UNIX只能够用于单一的Unix系统进程间通信,也称为本地socket,性能很好。AF_INET是针对Internet的,可以进行机器之间的通信,如常用的TCP和UDP等。

type: 指定通讯协议,常用的有SOCK_STREAM,SOCK_DGRAM等。SOCK_STREAM就是TCP协议,提供按顺序的、可靠的、双向的、面向连接的比特流。SOCK_DGRAM就是UDP协议,提供定长的、不可靠的、无连接的通信。

protocol: 因为指定了type,一般设为0即可。

返回值:成功返回文件描述符。失败时返回-1,错误码见errno。

bind(绑定本地IP和端口)

int bind(int sockfd, struct sockaddr *addr, int addrlen);

sockfd: 是由socket函数创建的文件描述符。addrlen: 是sockaddr结构的长度。

addr: 指定需要绑定的本地IP和端口,结构定义如下。其中sin_family一般为AF_INET, sin_addr设置为INADDR_ANY或IP地址,sin_port为端口号。sin_zero[8]用0来填充,目的是让sockaddr_in结构的大小与sockaddr结构的大小一致。

返回值:成功返回0。失败时返回-1,错误码见errno。

listen(监听端口)

int listen(int sockfd,int backlog);

sockfd: 为bind后的文件描述符。

backlog: 设置请求队列的最大长度。当此队列满了之后,客户端会收到ECONNREFUSED错误。

返回值:成功返回0。失败时返回-1,错误码见errno。

accept(响应客户端的连接请求)

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

accept是阻塞的,与客户端建立连接后返回,客户端地址保存在addr里。在TCP三次握手的第三次握手时返回。

sockfd: 为listen后的文件描述符。

addr: 是建立连接的客户端地址指针,为传出参数。

返回值:成功返回文件描述符。失败时返回-1,错误码见errno。

connect(请求与服务端建立连接)

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

connect是客户端请求连接服务端的函数。调用时发起TCP三次握手的第一次握手,在TCP三次握手的第二次握手时返回。

sockfd: socket返回的文件描述符。

addr: 用于指定服务器端的连接信息,其中sin_add是服务端的地址。

addrlen: addr的长度。

返回值:成功返回0。失败时返回-1,错误码见errno。

read(读取数据)

ssize_t read(int fd, void *buf, size_t count);

任何类型的文件描述符都可能通过这个函数读取数据,当然包括socket。

fd: 文件描述符。

buf: 读取的数据。

count: 读取的数据长度。

返回值:成功返回读取的字节数,返回0表示已经读完,小于0表示有错误,错误码见errno。

write(写入数据)

ssize_t write(int fd, const void *buf, size_t count);

任何类型的文件描述符都可能通过这个函数写入数据,当然包括socket。

fd: 文件描述符。

buf: 写入的数据。

count: 写入的数据长度。

返回值:成功返回写入的字节数,小于0表示有错误,错误码见errno。

recv和send

int recv(int sockfd, void *buf, int len, int flags)

int send(int sockfd, void *buf, int len, int flags)

recv/send函数提供了和read/write的功能相似。前面的三个参数相同,区别在于它们提供 了第四个参数(flags)来控制读写操作。如果flags为0,则和read/write完全一样,还可以组合使用如下值(只列出了部分),但实际上很少用到。

MSG_DONTROUTE: 是send函数使用的标志,这个标志告诉IP协议:目的主机在本地网络上面,没有必要查找路由表。这个标志一般用网络诊断和路由程序里面。

MSG_OOB: 表示可以接收和发送带外的数据(还没了解清楚)。

MSG_PEEK: 是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容。这样下次读的时候,仍然是一样的内容。一般在有多个进程读写数据时可以使用这个标志。

MSG_WAITALL: 是recv函数的使用标志,表示等到所有的信息到达时才返回。使用这个标志的时候recv回一直阻塞,直到指定的条件满足,或者是发生了错误。

  1. 当读到了指定的字节时,函数正常返回,返回值等于len。
  2. 当读到了文件的结尾时,函数正常返回,返回值小于len 。
  3. 当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)。

recvfrom和sendto

int recvfrom(int sockfd, void *buf, int len, unsigned int flags, struct sockaddr * from, int *fromlen)

int sendto(int sockfd, const void *msg, int len, unsigned int flags, struct sockaddr *to, int tolen)

recvfrom/sendto函数提供的功能和read/write也相似,前面的三个参数相同,flags同recv/send。

recvfrom负责从sockfd接收数据,如果from不是NULL,那么在from里面保存了数据来源的信息,也可以将from和fromlen 设置为NULL。

sendto负责向to发送数据,此时在to里面存储了收数据方的信息。如果to设为NULL和tolen设为0,功能同send。

close

关闭文件描述符,socket也是文件描述符。

常见错误码

  1. EINTR: 表示在写的时候出现了中断错误。
  2. EPIPE: 表示网络连接出现了问题(对方已经关闭了连接)。
  3. EWOULDBLOCK:
  4. ECONNREFUSED:
  5. EAGAIN:
  6. EBADF:
  7. ETIMEOUT:
  8. ECONNREST:

常用工具

netstat, ping, telnet, ss, nslookup, dig, sar

参考

  1. TCP链接状态:http://liudonghua.net/tcp-connect-and-close/
  2. http://www.cnblogs.com/RascallySnake/archive/2012/01/04/2312564.html