学习网络编程时候用到好多函数,初识之后有了个印象,然而每次再相遇还是得面向搜索引擎编程,所以打算记录一下。因为自己太菜了,所以有些地方难免表达错误,尽量改进提高。
文章目录
概述
1. 网络编程基本API
1.1 socket
网络编程肯定得先有个套接字,调用socket
函数创建套接字。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//成功时返回文件套接字,失败返回-1
domain
表示用哪个地址族,比如 AF_INET
(表示IPv4)、AF_INET
(表示IPv6)。
type
参数指明套接字类型,运输层协议用TCP的话就写个SOCK_STREAM
,表示固定长度、有序、可靠、面向连接的字节流。如果用UDP,就写个SOCK_DGRAM
,表示固定长度的无连接不可靠的报文传递。
proctocol
参数为协议类型,因为前面 type
选了SOCK_STREAM
或SOCK_DGRAM
,这里直接写个 0 表示默认就行了。
注:这里就不把全部的可用选项贴出来了。
这里要提一下可以把套接字看成文件(其实就是!),文件可以搞的操作,像创建、删除、输入、输出,对套接字也可以玩。其实我个人为了方便,把网络编程中的通信理解成就是在对套接字这个文件进行输入输出操作罢了,不过同时要注意文件描述符这个东西。
1.2 bind
用bind函数将套接字与地址关联。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
//成功时返回0,失败时返回-1
sockfd
是socket
函数返回的套接字文件描述符;
addr
是指向套接字地址结构的指针;
len
是套接字地址的长度;
有必要说明套接字地址的结构,Linux下通用的套接字地址结构体的定义为:
#include <bits/socket.h>
struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
}
其中sa_family
为协议族类型的变量,sa_data
存放套接字地址值。
直接用通用的结构体设置IP地址和端口号不方便,一般用sockaddr_in
(IPv4)和sockaddr_in6
(IPv6)进行地址设置,然后在实际使用时强制转化成通用结构。
#include <arpa/inet.h>
struct sockaddr_in{
sa_family_t sin_family;//地址族,AF_INET
uint16_t sin_port;//端口号
struct in_addr sin_addr;//IPv4地址结构体
}
struct in_addr{
uint32_t s_addr;//IPv4地址
}
struct sockaddr_in6{
sa_family_t sin6_family;//地址族,AF_INET
uint16_t sin6_port;//端口号
uint32_t sin6_flowinfo;//流信息,应设置为0
struct in6_addr sin6_addr;//IPv6地址结构体
uint32_t sin6_scope_id;//SCOPE ID (我不懂)
}
struct in6_addr{
uint8_t s6_addr[16];//IPv6地址
}
注意只有在服务器端才要给接收客户请求的套接字关联地址。在客户端,对于客户端套接字,就没必要用bind
关联客户端地址,在用connect
函数建立连接时系统自动分配了。之所以这样搞,是因为服务器端口应公共周知,连接时需要用确定的端口号。客户端套接字端口号自动分配就可以了,自己指定反而容易冲突。
1.3 listen
服务器端进行了套接字与地址关联的关联后,调用listen
函数后,服务器端就可以监听套接字。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//成功时返回0,失败时返回-1
sockfd
即为被监听的套接字,调用listen
函数后,该套接字就能接收连接请求。
1.4 accept
调用listen
函数之后,服务器端使用accept
函数接受连接请求并建立连接。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr , socklen_t *addrlen);//若成功返回非负描述符,若出错,则返回-1
//成功时返回套接字文件描述符,失败时返回-1
sockfd
为监听套接字;
addr
用于获取发出连接请求的客户端套接字地址;
addrlen
用于指出上述地址的长度。
服务器端调用该函数返回新的套接字文件描述符,该套接字连接到客户端,服务器端借助该套接字与客户端通信。
1.5 connect
bind
、listen
是服务器端为接收连接请求做准备,最后通过accept
接受连接请求。而客户端则是通过connect
函数发起连接。
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
//返回:若成功则为0,若出错则为-1
sockfd
为客户端套接字文件描述符;
addr
为服务器监听的套接字地址;
addrlen
用于指定上述地址的长度。
客户端调用该函数使客户端套接字与服务器端套接字建立连接后,之后就可以借助sockfd
来与服务器端通信。注意在客户端sockfd
没有绑定到一个套接字地址,调用connect
函数后会绑定一个默认地址。
1.5 close和shutdown
通信结束后要关闭连接,套接字像是文件,可以用close
来关闭连接。
#include<unistd.h>
int close(int fd);//fd为文件描述符
//成功返回0,失败返回-1
注意该函数的作用只是将对应的文件描述符的引用计数减 1。减到 0 才算是真的关闭,也就是说多进程的时候要用好几次close
。
此外还可以调用shutdown
函数关闭连接,该函数还能实现连接的半关闭,另外该函数还能立即终止连接,并非将所谓的引用计数减 1。
#inlcude<sys/socket.h>
int shutdown(int sockfd,int howto)
//成功时返回0,失败时返回-1
sockfd
为待关闭的套接字文件描述符;
howto
指定采取何种关闭方式。可选值有:SHUT_RD
关闭输入;SHUT_WR
关闭输出和SHUT_RDWR
:同时关闭输入输出。
2. I/O复用
2.1 select
通过select
函数,我们等待所感兴趣的文件描述符将对应的事件准备就绪。这话很难描述,其实就是比如有个客户要给我们服务器传来信息,而我们要读取这些信息,那么在建立连接后,服务器得到新的文件描述符,就要守着这个文件描述符,看它是否准备好被我们进行读操作。(应该是这样理解吧)
#include <sys/select.h>
int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);
//成功时返回就绪的文件描述符数目,失败返回-1
maxfdpl
表示所感兴趣的文件描述符总数;
readset
、writeset
、exceptset
,分别指向我们所感兴趣的可读、可写、处于异常条件的文件描述符集合。这三个指针指向的对象均为 fd_set
结构体类型。该结构体中有一个数组,数组的每个位标记一个文件描述符。fd_set
能容纳的文件描述符数量受限(1024)。对于fd_set
结构,有以下几个接口可以对其进行操作。
#include <sys/select.h>
FD_ZERO(fd_set *fdset);//清除fdset的所有位
FD_SET(int fd,fd_set *fdset);//设置fdset的位fd
FD_CLR(int fd,fd_set *fdset);//清除fdset的位fd
int FD_ISSET(int fd, fd_set *fdset); //测试fdset的位fd是否被设置
在声明了一个文件描述符集后先用FD_ZERO
把这个文件描述符集置0。在调用select
函数前,要用FD_SET
对我们感兴趣的文件描述符的位进行设置。从select
返回时用FD_ISSET
测试某个位是否打开,就是判断对应的文件描述符是否就绪。
timeout
指定超时时长,设置为NULL
表示一直阻塞。对timeval
结构的参数进行设置可以设置超时时长。timeval
结构体定义如下。
struct timeval{
long tv_sec;//秒
long tv_usec;//微秒
}
通过向select
函数传入上述参数,函数返回时我们就可以知道我们所感兴趣的哪些文件描述符已经准备就绪,例如某个文件描述符已经可读了。之后就能调用对应的I/O函数进行通信,比如对可读的文件描述符进行读取。
2.2 poll
poll
函数功能与select
函数相似。
#include <poll.h>
int poll (struct pollfd *fds, unsigned long nfds, int timeout);
fds
是一个指向结构体数组的指针,每个元素都是一个pollfd
结构体,该结构体定义如下:
struct pollfd {
int fd;//文件描述符
short events;//注册的事件
short revents;//返回的事件
};
可以看到select
函数要求分别传入可读、可写、异常的文件描述符集合,而poll
函数则是传入指向pollfd
结构体的数组的指针,每个结构保存感兴趣的文件描述符和对应的事件,此外内核还能告诉我们返回的事件,可以表示的事件类型如下表所示。
事件 | 能否作为events | 能否作为revents | 描述 |
---|---|---|---|
POLLIN | √ | √ | 普通或优先级数据可读 |
POLLRDNORM | √ | √ | 普通数据可读 |
POLLRDBAND | √ | √ | 优先级数据可读 |
POLLRDI | √ | √ | 高优先级数据可读 |
POLLOUT | √ | √ | 普通数据可写 |
POLLWRNORM | √ | √ | 同上 |
POLLWRBAND | √ | √ | 优先级数据可写 |
POLLERR | × | √ | 已出错 |
POLLHUP | × | √ | 已挂断 |
POLLHUP | × | √ | 描述符不是一个打开的文件 |
nfds
参数指定fds
集合的大小;
timeout
指定超时时长。-1表示一直等待,即阻塞;0表示不等待立即返回;设置大于0的值表示等待对应的时间,单位为毫秒。
2.3 epoll
epoll机制为Linux特有。其与select
和poll
在使用上有较大差别,epoll机制使用一系列的函数完成类似的任务。使用epoll机制首先需要调用epoll_create
函数创建一个文件描述符,用来标识一个事件表。epoll把感兴趣的文件描述符及对应事件注册到了这个表中。
#include <sys/epoll.h>
int epoll_create(int size);
size
指定事件表的大小。
epoll_ctl
函数用于对内核事件表进行操作。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd
:为epoll_create
返回的标识事件表的文件描述符,函数对这个事件表进行操作。
op
:指定操作类型,EPOLL_CTL_ADD
表示注册,EPOLL_CTL_MOD
表示修改,EPOLL_CTL_DEL
表示删除;
fd
:表示要操作的文件描述符;
event
:指定对应的事件,能描述的事件类型与poll
的相同。
struct epoll_event {
__uint32_t events; //epoll事件
epoll_data_t data; //用户数据
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
epoll机制使用epoll_wait
函数等待感兴趣的文件描述符发生对应的事件。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
//成功时返回就绪的文件描述符数目,失败时返回-1
epfd
为标识事件表的文件描述符;
events
从内核得到的事件集合;
maxevents
最大事件数量;
timeout
超时时间,含义与poll
的相同。
epoll机制与前面的方法的不同之处还在于,能将对应的就绪事件复制到events
参数所指向的数组中。这样一来,就不用花时间寻找就绪事件了。此外,epoll没有文件描述符数量的限制。
注意这里的表述有点变了,前面都是在讲文件描述符集合,现在变成了事件集合,其实都是一样的。文件描述符和所关心的事件是成对出现的,二者的数量是一样的。
注:LT模式和ET模式
水平触发(Level Trigger,LT)模式:默认的工作模式,即EPOLLIN。当epoll_wait检测到fd上有事件发生就会通知应用程序该事件的发生,若事件没有及时处理,在下次调用epoll_wait时还会通知事件的发生,比如没有读完或者写完,还会通知。即如果没有处理完那么就还会触发,所以不需要循环读写。
边缘触发(Edge Trigger,ET)模式:可通过往fd注册EPOLLET事件将工作模式改为ET模式,当epoll_wait检测到fd上有事件发生就会通知应用程序该事件的发生,若事件没有及时处理,则epoll_wait不会再通知事件的发生。即使没有读写完,也不会通知了,因此必须循环读写,把事件处理完。与LT模式相比,ET模式减少了事件重复触发的次数,提高了效率,但注意在ET模式下必须保证及时将事件处理完毕。另外要注意在ET模式下应使用非阻塞I/O。如果用阻塞I/O,循环读写完所有数据后,没有后续事件了,就会一直处于阻塞状态。使用非阻塞I/O,读写完后 errno 返回 EAGAIN,通过判断这个条件成立,就可以跳出循环停止读写了。
参考资料:
[1]《Linux高性能服务器编程》
[2]《Unix环境高级编程》