网络服务常见知识点
unp中以一个 echo 服务为例
被中断的系统调用
重试 accept
while (true) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0 && errno == EINTR) {
continue;
}
/* code */
}
重试 read write
{
again:
while ((n = read(sockfd, ptr, BUFSIZ)) > 0)
write(sockfd, ptr, n);
if (n < 0 && errno == EINTR) // EINTR
goto again;
else if (n < 0)
err_sys("str_echo: read error");
}
ssize_t readn(int fd, void *buf, size_t count) { // 可使用 recv() 与 MSG_WAITALL 代替
size_t nleft = count;
ssize_t nread;
char *bufp = (char *)buf;
while (nleft > 0) {
if ((nread = read(fd, bufp, nleft)) < 0) {
if (errno == EINTR) { // 系统调用被捕获信号中断
continue;
}
return -1;
} else if (nread == 0) {
return count - nleft;
}
bufp += nread;
nleft -= nread;
}
return count;
}
ssize_t writen(int fd, const void *buf, size_t count) {
size_t nleft = count;
ssize_t nwrite;
char *bufp = (char *)buf;
while (nleft > 0) {
if ((nwrite = write(fd, bufp, nleft)) < 0) {
if (errno == EINTR) {
continue;
}
return -1;
} else if (nwrite == 0) {
continue;
}
bufp += nwrite;
nleft -= nwrite;
}
return count;
}
SIGCHLD 僵尸进程
子进程结束变成僵尸进程,并给父进程发送一个SIGHLD信号,该信号默认忽略,只有当父进程调用wait或waitpid后子进程资源才被回收。
一个进程中所有线程的信号处理操作相同,每个线程可以有独自的 sigmask,使用 sigaction 指定信号处理函数时可指定 sigmask。
void sigchld_handler(int sig) {
// 此处不能单单使用 wait
// 当多个SIGCHLD信号同时到达时信号处理函数只被调用一次
// 未被处理的信号被忽略而不是排队
// 使用循环反复处理僵尸进程
// 使用 waitpid(-1, NULL, WHOHANG) 中-1表示所有子进程,WNOHANG表示当没有僵尸子进程时函数不阻塞
while (waitpid(-1, NULL, WNOHANG) > 0) {
/* code */
}
}
{
struct sigaction act;
act.sa_handler = sigchld_handler;
act.sa_flags = SA_RESTART; // SA_RESTART 指定syscall被中断后有系统恢复; SA_INTERRUPT 指定被打断后不恢复 不是每个版本都支持此宏
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, nullptr /*old action*/);
}
accept 返回前连接中止
服务器调用 listen,客户端调用 connect,此时客户端发送一个 RST 包,服务端调用accept返回一个 EPROTO 错误,此时只要忽略并重试 accept 即可
服务器进程终止
// echo 服务客户端
while (fgets(buf, sizeof(buf), stdin) != NULL) {
writen(sockfd, buf, strlen(buf)); // write to server
if ((n = readn(sockfd, buf, sizeof(buf))) == 0) {
printf("the other side has been closed.\n");
exit(0);
}
}
当服务端关闭并发送 FIN 时,客户端可能阻塞在fgets,只有当输入结束后调用read时才能得知对方已关闭。
客户端实际上在应对两个描述符——套接字和用户输入,它不能单独阻塞在这两个源的某一个上,而是应该通过 select epoll 阻塞在其中任何一个,一旦杀死服务器子进程,客户就会被告知收到FIN
使用 select 阻塞在任意一个
// 注意:select epoll 不能和stdio混用
// io复用只从read系统调用层面检测是否有数据可读,而不管stdio缓冲区内是否有数据未读出
void str_cli(int fp_in, int fp_out, int sockfd) {
int maxfdp1;
int stdineof{0}; // 是否已经读到文件尾
fd_set rset;
char buf[BUFSIZ];
FD_ZERO(&rset);
while (true) {
if (stdineof == 0) {
FD_SET(fp_in, &rset);
}
FD_SET(sockfd, &rset);
maxfdp1 = max(fp_in, sockfd) + 1;
select(maxfdp1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset)) {
if (read(sockfd, buf, BUFSIZ) == 0) {
if (stdineof == 1) { // 正常结束
return;
}
printf("server terminated.\n");
return;
}
write(fp_out, buf, strlen(buf)); // 输出
}
if (FD_ISSET(fp_in, &rset)) {
if (read(fp_in, buf, BUFSIZ) == 0) { // 文件读到 EOF
stdineof = 1;
shutdown(sockfd, SHUT_WR);
FD_CLR(fp_in, &rset);
continue;
}
write(sockfd, buf, strlen(buf));
}
}
}
SIGPIPE 信号
客户端在读回任何数据前执行两次针对服务器的写操作,服务器子进程死亡后,第一次引发RST,第二次写时内核向该进程发送一个SIGPIPE信号。该信号默认终止进程,写操作会返回 EPIPE。
服务器关闭
使用 shutdown 函数可单方面关闭套接字,并指定读写
- 宕机,此时客户端将不断尝试,直至超时,返回 ETIMEOUT EHOSTUNREACH ENETUNREACH
- 崩溃后重启,客户端发送消息则会收到一个RST响应并返回一个 ECONNRESET 错误,客户应通过 SO_KEEPALIVE 等套接字选项保证用户不主动发数据也能检测出连接重置
- 服务器关机,unix关机时 init 进程发送SIGTERM信号给所有进程,等固定时间后发送 SIGKILL 信号强行终止
客户/服务器程序设计范式
标签:unp,范式,服务器程序,int,进程,nleft,read,sockfd,buf From: https://www.cnblogs.com/zhh567/p/17606424.html