socket是一种ipc方法(介于传输层和应用层的一组api),允许同一主机或网络连接的主机上的应用程序交换数据。
现代操作系统支持下列socket:
流(tcp)和数据报(udp)
SO_BROADCAST:允许广播
SO_KEPPALIVE:周期性测试连接存活
SO_LINGER:若有数据报发送则延迟关闭
l_onoff=0:发送完发送缓冲区的数据并发送FIN。
l_onoff=1,l_linger=0:那么当close某个连接时TCP将终止该连接,丢弃发送缓冲区和接收缓冲区的数据并发送一个RST到对端,避免了TIME_WAIT。
l_onoff=1,l_linger!=0:发送完发送缓冲区的数据并发送FIN,丢弃接收缓冲区的数据,如果在CLOSED前延滞时间到,返回EWOULDBLOCK错误。
SO_RCVBUF:接收缓冲区大小
SO_SNDBUF:发送缓冲区大小
SO_RCVLOWAT:接收缓冲区低水平标记
SO_SNDLOWAT:发送缓冲区低水平标记
SO_REUSEADDR:重用地址
TCP_NODELAY:禁止Nagle算法 TCP_MAXSEG:TCP最大报文长度
fnctl 修改描述符性质。
新的通用套接字地址结构sockaddr_storage。
字节操纵函数
bzero: void bzero(void *dest, size_t nbyte)
把目标字节串指定书目的字节置为0。
memset: void memset(void *dest, int c, size_t len)
把目标字节串指定书目的字节置c。
地址转换函数
- inet_aton: int inet_aton(const char *strptr, in_addr *addrptr) // 点分十进制->32位网络子节串二进制
- inet_ntoa: char* inet_ntoa(struct in_addr addr) // 32位网络子节串二进制->点分十进制
- inet_pton: int inet_pton(int family, const char *strptr, void *addrptr) // 点分十进制->32位网络子节串二进制
- inet_ntop: char* inet_ntop(int family, const void *addrptr, char *strptr, size_t len) // 32位网络子节串二进制->点分十进制
read, write 字节流套接字调用read和write输入或输出的字节数可能比请求的字节数少,这个现象出现的原因是内核中缓冲区已达到极限。
为什么诸如套接字地址结构长度的值-结果参数要用指针来传递?
指针和指针长度的内容传递给内核,内核知道到底需要从进程复制多少数据进来。原因:当函数被调用时,结构大小是一个值,它告诉内核该结构大小,这样内核在写的时候不至于越界。当函数返回时,结构大小又是一个结果,它告诉进程内核在该地址结构存储了多少信息。这种类型的参数称为值-结果参数。(既是输入参数又是输出参数)
为什么readn和writen函数都将void型指针转换成char型指针?
在ANSI C标准中,不允许对void指针进行算术运算如pvoid++或pvoid+=1等,需要转换为char类型指针才能对指针进行加减操作。
int socket(int domain, int type, int protocol);
- domain:与socket通信的domain。
- type:socket类型,SOCK_STREAM(TCP),SOCK_DGRAM(UDP)。
- protocol:通常指定为0,在RAW_SOCKET中为IPPROTO_RAW。
return: 新创建socket的文件描述符。
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
- sockfd:在socket()调用取得的文件描述符。
- addr:要socket绑定到的地址。
- addrlen:制定了地址结构的大小。
return:-1为绑定失败。
struct sockaddr {
sa_family_t sa_family; //地址族
char sa_data[14]; //socket地址
};
int listen(int sockfd, int backlog);
- sockfd:socket文件描述符。
- backlog:限制未决连接的数量(在调用accept()前收到connect()的连接)。
return: -1为监听失败。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd:socket文件描述符。
- addr:对端socket的地址结构。
- addrlen:对端socket地址结构的长度。
return:和对端连接的文件描述符。
当调用accept()时,会创建一个新的socket,并且由这个新创建的socket来与执行connect()的对等socket进行连接。(这个socket并不绑定到新的端口号上,而是复制监听socket的地址和端口号,在tcp四元祖中记录主机和对端socket的地址信息)。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:socket文件描述符。
- addr:要连接到socket的地址。
- addrlen:地址结构的长度。
一个socket可以使用close()系统调用来关闭或在应用程序终止后关闭,当对端读取完缓冲区的数据后,试图继续读取数据时会收到文件结束,试图写数据会收到SIGPIPE信号,系统调用产生EPIPE的错误。
终止一个流socket的连接,如果多个文件描述符引用了一个socket,那么所有描述符被关闭后才会终止(若调用shutdown()则可以强制关闭socket上的信道)。
socket上的标准系统调用: int read(fd, buf, bufsize); int write(fd, buf, bufwrite);
unix domain socket unix domain socket地址:struct sockaddr_un
struct sockaddr_un {
sa_family_t sun_family; //AF_UNIX
char sun_path[108]; //socket路径名
};
当用来bind() UNIX DOMAIN SOCKET的时候,会在文件系统中创建一个条目(所在目录需要可读和可写)。
权限:
int socketpair(int domain, int type, int protocol, int sockfd[2]);
一个进程创建socket对,fork()出来的子进程复制socket对的文件描述符的副本,与父进程的socket队进行通信。
与手工创建socket对相比:不会绑定到任意地址上。
Internet Domain Socket
Socket的readline实现:
ipv4:struct sockaddr_in
struct in_addr {
in_addr_t s_addr; //32位无符号整数。
};
struct sockaddr_in {
sa_family_t sin_family; //地址族
in_port_t sin_port; //端口号
struct in_addr sin_addr; //ipv4地址
unsigned char __pad[X];
};
名字与地址转换
资源记录 A:A记录把主机名映射为一个32位的IPV4地址。 CNAME:为常用的服务(FTP,WWW)指定CNAME记录。 静态主机文件:/etc/hosts
int getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo *result);
- host:主机名
- service:服务名
- hints:为如何选择getaddrinfo()返回的socket地址结构指定了更多的标准。
- return: 0为成功,失败为非零值。
addrinfo细节:
struct addrinfo {
int ai_flags;
int ai_family;
int ai_protocol;
int ai_socktype;
size_t ai_addrlen;
char *ai_cannoname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
}
释放addrinfo列表:freeaddrinfo()
void freeaddrinfo(struct addrinfo *result);
getnameinfo(const struct sockaddr *addr, socklen_t addrlen, char *host, size_t hostlen, char *service, size_t servlen, int flags);
- addr:socket地址
- addrlen:地址长度
- host:主机名
- hostlen:主机名长度
- service:服务名
- servlen:服务名长度
- flags:其他参数
- return: 0为成功,失败为非零值。
UNIX DOMAIN SOCKET与INET DOMAIN SOCKET比较:
connect
listen
int listen(int sockfd, int backlog)
内核为任何一个监听套接字维护两个队列
当客户端的SYN到达时,若队列是满的,则忽略该分节(如果返回一个RST,则客户端无法判断“该端口没有服务器在监听”还是“该端口有服务器监听,只是它的队列满了”)。
对一个TCP套接字调用close会导致发送一个FIN,随后是TCP连接终止序列,为什么父进程对connfd调用close没有终止客户端与它的连接呢? 因为每个文件描述符或套接字都有一个引用计数,当父进程调用fork()时,connfd在父进程和子进程间共享,父进程和子进程的connfd的引用计数都为2,当父进程关闭connfd的连接时,此时引用计数为1,当子进程真正地处理和释放后,引用计数才为0。
getsockname和getpeername
正常终止客户端和服务器步骤
服务器进程终止
解决方案:select检测RST(两个EOF)。 服务器关机 init进程给所有进程发送SIGTERM信号,等待固定时间(5~20秒),发送SIGKILL信号(不可被捕获)。
信号 信号就是告知某个进程发生了某个事件的通知,有时也称为“软件中断”。信号可以:
处理僵死进程:waitpid
socket服务器
每次只处理一个客户端,只有处理完后才能处理下一个客户端。
能处理多个客户端的请求。常见思路:
提供服务:
readn()和writen(),循环启用系统调用,确保请求的字节数总是能够得到全部的传输。
int shutdown(int sockfd, int how);
- sockfd:socket的文件描述符。
- how:关闭方式
SHUT_RD:关闭连接的读端。
SHUT_WR:关闭连接的写端。通过文件结尾告诉对端本地写端关闭。
SHUT_RDWR:先执行SHUT_RD,后执行SHUT_WR。
与close()区别:有额外文件描述符引用的时候也会真正的关闭套接字。
ssize_t recv(int sockfd, void *buffer, size_t length, int flags);
ssize_t send(int sockfd, const void *buffer, size_t length, int flags);
部分flags参数:
- MSG_DONTWAIT:非阻塞方式运行,如果没有数据可用,立即返回,错误码EAGAIN,通过fcntl()可以把套接字设置为非阻塞模式(O_NONBLOCK)。
- MSG_WAITALL:阻塞方式运行。
- MSG_NOSIGNAL:对端连接关闭时,不产生SIGPIPE信号。
避免了内核空间的上下文切换,将文件内容直接传送到套接字上。
getsockname(int sockfd, struct sockaddr *addr, socklen_t addrlen):返回本地socket地址
getpeername(int sockfd, struct sockaddr *addr, socklen_t addrlen):返回对端socket地址
SO_REUSEADDR:当tcp端口释放时,无需等待TIME_WAIT即可直接重用地址。 SO_LINGER:close立即返回,但是当发送缓冲区中还有一部分数据的时候,系统将会尝试将数据发送给对端。SO_LINGER可以改变close的行为。
IO复用:预先告知内核,使内核一旦发现进程指定的一个或多个IO条件就绪(输入准备被读取,或描述符能承接更多的输出),它就通知进程。 输入操作的两个阶段
阻塞IO模型
recvfrom->无数据报准备好->等待数据->数据报准备好->数据从内核复制到用户空间->复制完成->返回成功指示
非阻塞IO模型
recvfrom->无数据报准备好->返回EWOULDBLOCK->recvfrom->无数据报准备好->返回EWOULDBLOCK->数据报准备好->数据从内核复制到用户空间->复制完成->返回成功指示
特点:轮询操作,大量占用cpu时间。
IO复用模型
select->无数据报准备好->据报准备好->返回可读条件->recvfrom->数据从内核复制到用户空间->复制完成->返回成功指示
信号驱动模型
建立信号处理程序(sigaction)->递交SIGIO->recvfrom->数据从内核复制到用户空间->复制完成->返回成功指示
异步IO模型
aio_read->无数据准备好->数据报准备好->数据从内核复制到用户空间->复制完成->递交aio_read中指定的信号
特点:直到数据复制完成产生信号的过程中进程都不被阻塞。
读
写
当某个套接字上发生错误时,它将select标记为可读且可写
fd_set
FD_ZERO(&set); /*将set清零使集合中不含任何fd*/
FD_SET(fd, &set); /*将fd加入set集合*/
FD_CLR(fd, &set); /*将fd从set集合中清除*/
FD_ISSET(fd, &set); /*在调用select()函数后,用FD_ISSET来检测fd是否在set集合中,当检测到fd在set中则返回真,否则,返回假(0)*/
运行进程同时检查多个文件描述符以找出它们任意一个是否可以进行io操作,系统调用select()和poll()进行多路复用。
EPOLLLT——水平触发 EPOLLET——边缘触发
原因:文件描述符存在大量输入,一次次读取不完,由于边缘触发只在状态变化时进行通知,因此socket可能发生长时间的等待而导致饥饿。
解决方案:让应用程序维护一个列表,存放着已被标记为就绪态的文件描述符,通过一个循环的方式不断处理,直至出现EAGAIN或EWOULDBLOCK。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- readfds: 输入就绪的文件描述符集合。
- writefds: 输出就绪的文件描述符集合。
- exceptfds: 异常发生的文件描述符集合。
return: 就绪的文件描述符数量。
timeout参数
struct timeval {
time_t tv_sec; //秒
suseconds_t tv_usec; //微秒
};
timeval参数:
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
return: 就绪的文件描述符数量。
pollfd结构
struct pollfd {
int fd; //文件描述符
short events; //请求事件
short revents; //返回事件
};
select()和poll()在套接字上通知的事件
当有输入数据来到指定的文件描述符时,内核向请求数据的进程发送一个信号,进程可以处理其他任务,通过接收信号以获得通知。
在文件描述符上注册事件函数,由系统监视这些文件描述符,当在文件描述符可就绪时,内核通知应用进程。
int epoll_create(int size);
- size:制定了我们想要通过epoll检查的文件描述符个数。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
- epfd:要修改的文件描述符
- op:执行的操作
- fd:
EPOLL_CTR_ADD:把fd添加到epfd的兴趣列表中。
EPOLL_CTR_MOD:修改fd上设置的事件。
EPOLL_CTR_DEL:将fd从epfd中移除。
- ev: 指向结构体epoll_event的指针,设置关注事件。
epoll_event定义
struct epoll_event {
uint32_t events; //epoll事件
epoll_data_t data; //用户数据
};
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
- epfd:文件描述符。
- evlist:包含就绪文件描述符信息(epoll_event)的数组。
- maxevents:最大事件数量。
- timeout:超时设定,-1阻塞,0非阻塞,>0等待。
return: 就绪的文件描述符数量。
epoll与信号驱动
Libevent
对各种io模型(select,poll,signal,epoll)进行透明的封装,支持/dev/poll和kqueue接口。
recv,send:允许通过第四个参数从内核到进程传递标志。 readv,writev:允许指定输入数据或输出数据的缓冲区变量。 recvmsg,sendmsg:具备发送和接收辅助数据的能力。
套接字超时
flags 说明 recv send MSG_DONTROUTE 绕过路由表查找 y MSG_DONTWAIT 仅本操作非等待 y y MSG_OOB 发送或接收带外数据 y y MSG_PEEK 窥看外来消息 y MSG_WAITALL 等待所有数据(readn) y
IO缓冲
IO模型
对应非阻塞的套接字:
面向连接和无连接指的都是协议,不是物理介质本身,而是说明如何在物理介质上传输数据的。 无连接:每个分组都是独立寻址,并由应用程序发送。 面向连接:协议实现维护了与后继分组有关的状态信息。 tcp:打电话。 udp:发邮件。
使用子网划分有助于防止路由表的增长,CIDR使得IP地址的分配更加有效,并使这些地址的层次化分配更加简单。
NAT:实现私有网络地址与全局网络地址间的映射。 NAT三种模式:
套接字提供了更简单、可移植性更好的接口。
TCP面向字节流,不存在消息边界,调用recv时,不会对TCP发送给它的数据量做任何假设。 粘包:由于TCP不存在消息边界,因此TCP发送的分组可能被对端一次性读取,造成粘包。
针对长时间的大数据的连接,TCP的性能会比UDP好得多。
TCP无法将连接的丢失立即通知应用程序。
检测死连接:
WAN比LAN更容易出现网络时延问题。
写入操作:把数据从用户缓冲区复制到内核,立即返回。不担保数据的正确发送。 写操作的错误时由读操作返回的,写操作只返回写调用时发生的明显错误。
通过shutdown来激活连接的有序释放,有序释放是在确保没有数据丢失的情况下拆除连接的一个过程。
inetd守护进程负责对连接或数据报进行监听,将套接字映射到stdin,stdout,stderr中。
wait|nowait:指明从inetd里头调用的服务是否可以自己处理socket。dgramsocket类型必须使用wait,而stream socket daemons,由于通常使用多线程方式,应当使用nowait。 wait 通常把多个 socket 丢给单个服务进程, 而 nowait 则 会为每个新的 socket 生成一个子进程。
派生出一个子进程来处理TTY连接的写操作,由父进程负责处理读操作。
将多个定时器复用到一个select定时器中去,用这个函数支撑函数timeout和untimeout,只要少量工作就可以对多个事件进行定时。
TIME-WAIT:拆除连接中发送最后一个ACK到连接关闭(CLOSED)的过程。 TIME-WAIT作用:维护连接状态,为耗尽网络中所有此连接的“走失段”提供时间,防止ACK丢失导致被动关闭的一端超时并重传FIN。如果此时连接关闭,TCP则会丢弃这条连接的记录,用RST(重置)来响应,对等实体会产生一个粗欧文状态,不会有序地终止。若此时端处于TIME-WAIT状态,则可以响应对端重传的FIN返回一个ACK。 提前终止TIME-WAIT:套接字的SO_LINGER选项。
防止服务器等待前一条连接的TIME-WAIT状态过期而重启服务器时发生Address already in use错误,重启一个之前处于TIME-WAIT状态的服务器。
Nagle算法:任意时刻,最多只能有一个未被确认的小段。 Nagle算法作用:防止在网络中传输大量的小报文导致网络泛洪。实现:在发送分组后,在发送剩余数据前等待ACK。 Nagle算法的规则:
禁用Nagle算法:针对对延迟容忍度低的实时应用。
big endian:最高字节在地址最低位,最低字节在地址最高位,依次排列。 little endian:最低字节在最低位,最高字节在最高位,反序排列。