网络编程最佳实践

网络编程最佳实践

socket描述符的引用机制

socket描述符引用机制,当父进程生成一个socket描述符然后fork一个子进程后,那么这个socket描述符的引用计数就+1了此后子进程close socket描述符只是减少引用计数并没有真正关闭描述符。如果希望真正关闭描述符需要使用shutdown函数

最佳实践:fork子进程后父进程立即close掉socket描述符

connect背后机制

客户端发起connect系统调用内核就会帮助我们建立三次握手中的最后一步,首先内核发送了一个syn的同步报文,服务器端响应这个syn报文,发送syn+ack报文给客户端,客户端接收到这个syn+ack报文后connect系统调用返回了,此时内核负责发送最后一个ack报文给服务器端

最佳实践:connect系统调用返回的错误有很多中,如果你需要知道关于connect连接失败的更多细节可以查看man手册针对每种错误进行处理,否则我推荐你直接perror打印错误字符串即可。

Ctlr+c/exit/return做了啥?

在客户端使用ctrl+c关闭客户端程序,或者是客户端调用了exit,或者是从main函数return背后做了啥?服务器端能否感知这些客户端退出,怎么去感这些客户端退出了? 这些都是网络编程中需要考虑的事情,其实在客户端Ctrl+c/exit/return中内核帮我们发送了一个FIN报文给服务器端了,服务器端对此报文进行了ACK响应,此时服务器处于CLOSE_WAIT状态,客户端处于FIN_WAIT_2状态,那么服务器端如何感知这一变化呢? 服务器端此时读取这个socket会返回0,返还0就表示这个socket的对端已经关闭了。那么服务器端就需要对其进行处理,最简单的莫过于close掉这个描述符,此时服务器端内核就会发送最后的那个FIN报文此时服务端处于LAST_ACK状态客户端接收到这个FIN报文后(此时由内核处理,应用程序已经终止了),会响应一个ACK报文然后自己变成了TIME_WAIT状态了。其实服务器端还有另外一种方法可以感知客户端已经关闭了下文将会介绍

最佳实践: 服务器端要针对read/recv一类的一系统调用的返回结果=0的情况作出处理,通过返回结果=0就可以知道客户端关闭了socket,此时服务器端就需要做相应的处理

多个子进程exit,该怎么办?

在处理多个并发连接的时候最简单的方法莫过于使用多进程的形式,一个进程处理一个连接,连接关闭后子进程就exit,那么子进程exit后,服务器端要做些啥呢,做过linux应用编程的应该都清楚子进程exit后是需要进行状态回收的,否则就会变成僵尸进程一直占用系统的pid资源,地址空间,内存等那么如何高效并且安全的对子进程进行回收就是一个问题了,如果只有一个子进程你完全可以一个wait搞定,但是wait是阻塞的,我不应该要主进程阻塞来对子进程进行回收,这样未免太浪费了,毕竟主进程还是有其他事情要做的,此时可以使用信号来实现,子进程结束后一般会向父进程发出SIGCHLD信号,只要在主进程对这个信号进行处理并wait即可,但是如果有多个子进程同时exit,那么此时需要考虑信号是否排队,信号是否是实时的,此时对子进程进行回收的时候最好使用waitpid非阻塞进行回收。

最佳实践: 工程中可以使用的子进程回收代码

 void sig_child(int signum)
{
        pid_t pid;
        int stat;
        //WNOHANG 表示waitpid是非阻塞的 -1代表回收所有退出的子进程
        while((pid = waitpid(-1,&stat,WNOHANG)) > 0)
        {
                        //deal_with someting
        }
        return;
}

系统调用执行了一半被中断了?

系统调用中有一些系统调用被称为慢速系统调用(调用时阻塞等待IO的系统调用一般都是慢速的),很容易被信号打断,有的系统这些系统调用打断后会自动重启的,但是有的系统则不然,为了写出可移植的程序就需要让被打断后的系统调用重新执行,如何知道系统调用被信号打断了呢?,可以通过判断系统调用的返回错误码来进行判断,当系统调用的返回错误码是EINTR

最佳实践: 系统调用重启的常见代码技巧(注意connect不能被重启)

情景一: 
for(;;){
    int ret = systemcall()
    if(ret < 0)
        if(errno == EINTR)
            continue  //被中断重新执行
        else{
            cout << "have error" << endl;
            break;
        }
}
情景二:

again:
    int ret = systemcall()
    if(ret < 0)
        if(errno == EINTR)
            goto again;
        else{
            cout << "have error" << endl;
            exit(1);
        }

accept干了啥?什么时候返回?

我们知道accept是阻塞的,那么accept因为什么阻塞呢,背后干了啥,其实accept做的唯一一件事就是从内核的连接队列中取出一个连接然后返回这个连接对应的描述符。内核中有两个个队列,一个是客户端发送syn到服务端,服务端发送syn+ack后等待客户端的ack的这些连接叫做等待队列,还有一个连接队列就是客户端和服务器端已经完成三次握手的连接

最佳实践: accept需要进行自重启

accept返回前连接终止是啥情况?

这种情形在比较繁忙的web服务器中还是出现过的,具体是怎么发生的呢?,当服务器端accept从连接队列取出一个连接之前,客户端向服务器端发送一个RST报文(撤销连接)这导致accept的时候会返回错误(因为连接被撤销了),不同的系统平台错误码不同,POSIX要求返回ECONNABORTED这个错误码。

服务器端进程终止该怎么办?

这和客户端关闭socket本质上没有太大区别,都是完成了四次挥手的一半,只是这里服务端是主动端,客户端是被动端,那么此时客户端同样需要感知服务器端关闭socket了。 此时的情况就比较复杂了,有这样几种情况,如果客户端继续读这个socket那么会返回0,客户端根据返回0就可以知道服务器端已经关闭socket了,这点和客户端主动关闭时一样的,但是如果客户端继续往这个socket写数据那么会发生什么情况呢(写入是成功的),此时服务器端会发送一个RST报文给客户端,然后客户端再次读取的时候就会返回错误,错误码是ECONNRESET(连接被重置)[这是UNP书上的解释]经过我验证发现新的内核已经不是这样的了,对于半关闭了的套接字写入第一次是成功的,无论读取多少次都是成功返回0的,但是当写入第二次后写入失败产生SIGPIPE信号。

recv的man文档中是这样说的:
For TCP sockets, the return value 0 means the peer has closed its half
side of the connection.
关于这个问题的一个讨论:
http://fixunix.com/unix/84635-how-do-you-get-econnreset-recv.html

服务器崩溃了怎么办?

在服务器端和客户端正常通信的情况下,服务器此时奔溃(不是服务器端进程崩溃),此时客户端向服务器端发送数据会出现什么情况呢,发送完数据后再recv接收数据又会出现什么情况呢? 此时客户端向服务器端发送数据是可以发送成功的,你在客户端查看客户单的套接字的连接状态是ESTABLISHED的(服务器挂了,内核也没来得及发送FIN报文,如果是服务器端进程挂了,内核是负责发送FIN报文给客户端的) 但是当客户端通过recv接收数据的时候被阻塞了(数据虽然是发送成功了,但是内核始终没有接收到服务器端响应的ACK报文,所以内核会不断的进行数据重传,大概持续9分钟),recv阻塞一段时间后将会返回错误。错误码有几种情况:主机不可达(EHOSTUNRAEACH 或者是 ENETUNREACH)又或者是主机可达但是主机不响应(ETIMEDOUT)在服务器端崩溃的这种情况下客户端需要立即知道,如果上述情况中在服务器崩溃后没有再次写入数据的话,recv会一直阻塞。因为此时客户端根本不知道服务器崩溃了,所以存在两个问题,第一个问题就是如何在服务器崩溃后立即知道服务器崩溃了,而不是等再次写入数据的时候才知道,第二个问题是服务器崩溃接收数据阻塞时间太长(数据重传的时间),应该让recv具有超时机制,避免在这种情况下阻塞太长时间。

最佳实践:recv要具超时功能,设置套接字的SO_KEEPALIVED选项,可以在服务器崩溃的时候检测到服务器出问题了。

服务器崩溃后又重启了?

当服务器和客户端一切正常后,服务器突然崩溃,不过服务器立马重启,此时客户端再发送数据给服务器端会发生什么情况呢?服务器崩溃重启后会丢失之前所有的连接信息,所以如果此时有客户端发来数据服务端会响应一个RST报文,此时客户端写入是成功,客户端的写入会导致服务端发送RST报文,那么此后客户端使用read/recv读取数据的时候就会返回0,表示服务器端连接被关闭了。如果客户端再次写入就会引发SIGPIPE信号的产生。这种情况和服务器端进程终止的情况是一样的。

最佳实践:参考上文对SIGPIPE信号的处理

服务器主机关机了?

这个和服务器崩溃有何区别呢? 如果是服务器主机关机的话,init进程是会向所有进程发送SIGTREM信号(可以捕捉),然后等待一段时间(5~20)会给仍然在运行的进程发送SIGKILL(这个信号无法捕捉)如果是服务器主机关机,那么子进程就会被信号杀死,导致内核发送FIN报文,此后的情况就和服务器端进程终止的情况是一样的。

最佳实践: 捕获SIGTREM信号,做一些善后处理工作。

文本数据 or 二进制数据?

无论客户端和服务器端的字节序是什么,机器的体系结构是什么,只是传递文本字符串(两端的字符集相同即可)的话是没有任何问题web服务器就是这样的一个场景(传递文本)。传递文本带来的问题就是传输效率比较低。那么如果传递二进制数据呢,传递二进制数据虽然效率上高了,但是同样也带来了很多问题

  • 客户端和服务器端的字节序问题,
  • 不同OS对数据类型的位数的表示不同
  • 不同OS的数据对其机制不同

考虑到上面的问题,传递二进制数据是不明智的,但不是说没有办法解决。
1. 将二进制数据转换为文本字符串
2. 显示定义所支持的数据类型的二进制(位数,大端或小端字节序)并以这样的格式在服务器和客户端之间传递

小结

TCP/IP的编程中API的使用时基本功,API背后和TCP/IP的状态迁移的关系要搞懂。要学习如果处理TCP/IP编程过程中所遇到的这些边界条件和特殊情况才是重点。下面是一张socket边界错误的汇总(也就是本文的一个概括)
这里写图片描述

©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值