裸建服务器的问题较多,但是作为后端程序员依然有必要了解,以便更深刻地理解为什么要使用IO多路复用模型、异步模型等。
前导知识
网络IO:在计算机网络中进行数据的输入/输出(Input/Output)的操作,可以是由本地程序发送给本地程序,也可以是由多台不同主机上的程序进行操作。
网络IO的步骤:1、建立连接。2、接收数据(I)/准备数据(O)。3、数据处理(I)/发送数据(O)。
IP与网卡:网卡是接收数据的硬件设备。一台主机可能有多张网卡,一张网卡一般对应一个IP地址。IP地址127.0.0.1,一般用于同一张网卡之间内部进行IO。IP地址为0.0.0.0,一般表示本机所有的网卡的IP的集合。IP地址为非127.0.0.1,非0.0.0.0,一般是某张网卡的IP。
建立服务端——基于TCP协议
建立一个发送什么什么,就会返回什么数据的服务端。
建立服务端套接字
首先需要建立套接字。这个套接字有什么作用呢?打个比方,一个客户要和服务中心建立通信关系,那么客户先给这个“服务端套接字”发送消息说“我想和服务中心建立通信”,这个“服务端套接字”走了一下流程,发现没问题,于是就允许二者建立他们的通信线路。
int socket(int domain, int type, int protocol);第一个参数表示地址族,第二个参数表示套接字类型(一般决定是tcp协议还是udp协议),第三个参数通常是0,表示根据第二个套接字自动选择协议。
设置服务端套接字的信息
我们一般先把信息存储在一个结构体实例中,然后通过再把这个结构体实例通过bind函数绑定给套接字。如果是ipv4地址用struct sockaddr_in,如果是ipv6地址用struct sockaddr_in6。INADDR_ANY表示0.0.0.0这个IP。套接字一般分为两部分,一部分在用户空间,一部分在内核空间。在用户空间体现为一个数字,一个文件标识符,在内核空间被实现为一个结构体,用于管理网络通信的底层细节。
把套接字信息与套接字绑定
成功返回0,失败返回-1。
让套接字进入监听状态
第二个参数表示等待队列的长度,也就是最多能同时监听多少个来自客户端的接入请求。在等待队列中的监听请求已经进入了被监听状态,但是流程未走完。
客户端发起连接请求
推荐一个客户端连接工具:TCP/UDP Net Assistant
建立连接是不需要代码显式参与的。
在服务端用linux命令查看连接成功与否
可以看到服务端的监听端口有两个状态,一个是LISTEN,一个是ESTABLISHD。ESTABILISHD那一行记录了客户端的IP和port。
服务端接收数据
网络连接建立成功之后,服务端已经能接受信息了。但是还需要专门的操作把消息读取出来。
首先需要建立一个新的套接字,把客户端与服务端套接字的通信,转移到另一个套接字上去,并实现专门的接受报文的功能。
建立一个结构体,用于存储客户端的相关信息,比如IP,port等。补充:clientid是在stdin,stdout,stderr,socketfd的基础上依次增加的。
然后就调用recv函数通过新的套接字接收数据了。需要先准备好一块接受数据的空间。如果没有数据发送,会被阻塞住。
返回值是接收到的数据的长度。
发送接收到的数据
sned函数第二个参数是发送的数据的长度。如果buffer数据没准备好会被阻塞住。
clientfd的回收
查看连接的状态,刚调用close的时候,连接先进入timewait状态,因为服务端可能需要一些时间处理未完的数据,等待一段时间后会彻底关闭连接。
如果服务端出现了大量的timewait,原因大概率是服务端因为某些原因崩溃、不运行等,与大部分甚至所有客户端进行断开操作。当然,也有可能是主动断开。
用循环改进服务端——能持续接受并返回一个客户端的报文
上一版的代码是只能一次性接受并发回数据,将数据处理这部分改为while循环,可实现能持续接收并返回一个客户端的报文。
但是上述改进有一个问题:如果断开了连接,没办法进行close(fd)。因此需要加一个判断,判断客户端断开了连接,并跳出循环,进行close操作。
如果客户端已经断开了连接,但是close操作被阻塞住,那么tcp连接会进入close_wait状态。Close wait状态下客户端能重连接,但是服务端没法再进行数据处理了。
插图
用多线程改进服务端——能持续接收并返回多个客户端的报文
来一个连接,就建立一个线程进行处理。
因此,如果有1w个,就建立1w个线程。会非常地不利于大量并发。也就是传说中C10K的问题。
基于上述原因,建议服务端使用一些模型。