第十二章 IPv4和IPv6
一图解释:
第十三章 守护进程和inetd
守护进程:在后台运行且不与任何终端关联的进程。
syslogd:这个守护进程创建Unix域数据报套接字绑定/dev/log,等待接受日志信息,不过推荐使用syslog函数。
1 | void syslog(int priority, const char *message, ...); |
priority是以下两者位或:
message类似printf。
关于daemon_init:
- fork,保证子进程后台运行且不是一个进程组的头进程;
- setsid,子进程变成新会话和新进程组的头进程;
- fork,保证子进程不会获得控制终端;
- 更改目录,关闭描述符,重定向描述符;
- 用syslogd处理错误。
关于inetd:
- 简化部分守护进程的编写;
- 单个进程取代每个服务一个进程,减少了进程总数;
- 读取/etc/inetd.conf配置;
第十四章 高级I/O函数
如何设置I/O超时?
- 调用alarm,通过SIGALRM中断系统调用;
- 使用select提供的超时机制等待I/O,用于connect时要求非阻塞;
- 使用SO_RCVTIMEO, SO_SNDTIMEO套接字选项。
recv和send
1 | ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags); |
比起read和write区别在flags上,如下:
readv和writev
1 | ssize_t readv(int filedes, const struct iovec *iov, int iovcnt); |
分散读和集中写,iov是指向struct iovec的一个数组。
recvmsg和sendmsg
1 | ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags); |
- msg_name, msg_namelen类似recvfrom, sendto的第五、六个参数;
- msg_iov, msg_iovlen类似readv, writev的第二、三个参数;
- 区别flags和msg_flags,两者在接受和发送中有所不同;
辅助数据
利用sendmsg, recvmsg中msghdr的msg_control, msg_controllen发送接受。
第十五章 Unix域协议
Why use it?
- 同一主机上比TCP快;
- 能传递描述符;
- 能提供额外的安全检查;
1 | struct sockaddr_un{ |
- 路径必须是绝对路径名;
- connect连接的路径名必须满足:
- 存在该路径名;
- 必须是一个套接字;
- 有与之关联的打开的描述符;
- 类型相符合;
- connect发现监听套接字队列满直接返回ECONNREFUSED错误,不会像TCP一样重试;
- 发送数据报必须手动绑定路径名,系统不会自动绑定路径名,否则接收端无法应答;
1 | //创建两个连接的套接字,仅适用于Unix域套接字 |
描述符传递:
- 创建Unix域套接字;
- 发送进程创建msghdr,在其中填上描述符,发送;
- 接受进程利用recvmsg接受描述符;
注意,描述符传递并不是传递描述符数值,而是在发送过程中创建一个新的描述符,这个新的描述符引用相同的文件表项,新描述符的数值和发送描述符数值没有任何关系。
第十六章 非阻塞I/O
将一个套接字设置为非阻塞后,对其调用read, readv, recv, recvfrom, recvmsg, write, writev, send, sendto, sendmsg, accept若无法满足条件,进程不会睡眠,而是调用会返回一个EWOULDBLOCK,对于connect来说,连接通常无法立刻建立,但会激发三次握手,返回EINPROGRESS。
使用方法通常如下:
1 | int val = fcntl(fd, F_GETFL, 0); |
非阻塞connect:
用途:
- 将三次握手的时间利用起来;
- 同时建立多个连接;
- 利用select缩短connect的超时;
注意:
- 非阻塞connect通常无法立刻完成,返回EINPROGRESS,但若是同一主机,连接通常立刻完成。
- 利用select如何判断connect成功是个问题:
- 套接字可写不可读意味着connect成功;
- 套接字可读可写意味着发生错误或者连接成功且对端已发送数据,应当再判断一下errno。
- 移植问题(这里不考虑
- 如果非阻塞connect可能被中断,且connect不被重启,此时只能使用select判断connect是否成功,因为我们不能再次调用connect。
非阻塞accept:
若用select检测监听套接字的可读性(是否可accept),既然select返回的是监听套接字可读,那么accept就不可能阻塞吗?答案是否定的,如果在select返回和accept之间客户发送RST终止连接,则连接被移除已连接队列,这时服务器调用accept就会阻塞,为了避免这个问题,就应当在select之前把监听套接字设为非阻塞,并在之后的调用中注意这一点。
第十七章 ioctl
ioctl是对设备I/O通道进行控制的函数。
第十八章 路由套接字
路由套接字可以管理路由表、接口列表,获取相关信息。
第二十章 广播
竞争状态的相关思考:
- 如果有多个进程访问共享数据,结果取决于进程的执行顺序,那么此时竞争问题就会出现;
第二十二章 高级UDP编程
- 只有UDP支持广播和多播;
- UDP连接成本小;
- UDP不可靠,无流量控制;
- 简单请求和应答可以使用UDP,但应用程序应当自行错误检测;
UDP的可靠性
- 超时和重传
- 序列号
并发UDP服务器
- 如果UDP只发送一个应答,简单fork让子进程处理客户就好。
- 如果UDP和客户交换多个数据报,由于客户只知道服务器的众所周知端口,当主进程接受到一个数据报时不好判断数据报的来源,此时的解决办法是为每个客户创建一个新的套接字,并让客户将之后的数据包发送至这个新的套接字。
第二十四章 带外数据
TCP并没有真正的带外数据,不过提供了紧急模式,一些注意点:
- 收到的TCP首部虽然指出发送端进入紧急模式,但紧急指针指的数据却不一定已经发送;
- 即使因为流量控制暂停发送数据,紧急通知仍会发送;
- 每个连接只有一个带外标记,新到达的标记会覆盖之前的标记;
- 除非指定SO_OOBINLINE,新的带外字节会覆盖旧的带外字节;
第二十五章 信号驱动式I/O
- 设置SIGIO信号的信号处理函数;
- 设置套接字属主(fcntl F_SETOWN);
- 开启信号驱动式I/O(fcntl F_SETFL);
SIGIO信号的产生:
- 对于UDP:
- 数据报到达;
- 发生异步错误;
- 对于TCP:
- 监听套接字连接请求完成;
- 断连请求发起;
- 断连请求完成;
- 连接半关闭;
- 数据到达;
- 数据发送;
- 发生异步错误;
信号处理函数无法判断套接字的情况,于是信号式I/O对于TCP基本没用,UDP还有点用,不过只是一点。
第二十六章 线程
线程可以理解为轻量进程,同一进程有多个线程,这些线程共享全局内存,也就是共享全局变量、描述符、信号处理函数、工作目录,但每个线程有各自的TID、寄存器、栈、errno、优先级。
创建和终止
1 | //类似fork,attr是线程属性,func是函数指针,arg是函数参数 |
线程特定数据
每个线程支持有限数量的线程特定数据:
调用pthread_key_create创建一个键,并指定析构函数;
调用pthread_getspecific获取特定线程的键值(指针);
分配所需内存,并将内存地址赋给指针;
线程终止时,扫描pkey数组,为非空元素调用key数组中相对应的析构函数;
互斥锁和条件变量
1 | int pthread_mutex_init(phtread_mutex_t *mptr, const pthread_mutexattr_t *attr); |
1 | int pthread_cond_init(phtread_cond_t *cptr, const pthread_condattr_t *attr); |
为什么条件变量必须关联一个互斥锁:
- 既然是条件变量,通常和一个变量有关,而这个变量肯定是共享的,因此这个变量肯定需要一个互斥锁;
- wait函数是原子操作,当它阻塞时,必须要解锁这个互斥锁,如果不传给wait函数互斥锁自己手动解锁互斥锁,就不是原子操作了;
第二十八章 原始套接字
原始套接字可以直接读取ICMP分组,可以处理内核不处理的协议字段的数据报,还可以自己构造数据报。
第三十章 C/S服务器程序设计范式
- 迭代服务器
- 并发服务器
- 多进程
- 有进程池
- 无进程池
- 多线程
- 有线程池
- 无线程池
- 多进程