前一阵子想写一个服务器,嗯,一开始是想写一个电商平台来着.............
然后就开始学,慢慢的觉得自己需要学习的东西真的还有很多很多,比如用长连接还是短连接,长连接的话怎么节省系统开销,心跳包的设置,避免产生大量小包的nagle算法,html的token怎么用,还有cookie,io复用,计时器,数不清的内部算法........So,坐下来学!
然后前天写出了这个服务器的基本的样子,昨天调试时候把bug改了改,于是一个基本模型算是出来了(嗯一个没写计时器的破模型.......最近打算把计时器写一写)。
下面写一下我的学习过程。
套接字框架
首先编程中通信的基本是使用套接字。套接字分为客户端和服务端。
服务端常见的流程是socket() ->bind() ->listen() ->accept()这几个函数。
(当然也有例外,比如有一种方法是客户端进行bind之后connect,这里暂时不谈。)
socket函数
socket函数指定地址描述,套接字类型,指定协议并返回一个描述符。地址描述一般来说只能选择使用AF_INET;套接字类型有tcp,udp和原始套接字比较常用,这里我使用了tcp协议,所以参数是SOCK_STREAM;最后一个参数可以不设置,我直接将其置为0。
bind函数
bind函数将刚才返回的文件描述符绑定在一个端口。并设置一个sockaddr_in变量存储服务器本身的IP和端口等信息。
listen函数
listen函数对刚才绑定的端口进行监听,并设置一个队列长度。该队列长度事实上在内核中控制了两个队列,分别是未连接队列和已连接队列。未连接队列储存的是已经发送连接请求,但是尚未完成三次握手的客户连接,当该队列中的连接完成三次握手之后,将进入已连接队列的尾部;已连接队列储存的是已经完成三次握手但是尚未被accept函数接收的客户连接。
accept函数
accept函数从已连接队列队首获取连接,将该连接的对端信息(IP和端口等)储存在一个sockaddr_in变量中,并返回一个描述符(此次客户连接的描述符)。
端口重用
作为一个服务器,当然不能随意ctl+c掉,但是如果服务被关闭(比如服务器崩了)之后,立即重新启动服务,会出现一个无法绑定端口的错误。这是因为刚才的端口处在了TIME_WAIT的状态,内核中该状态将会维护两个MSL(maximum segment lifetime)的时间,linux下大约是1分钟左右,在此期间该端口不可再次绑定。
但是我们是服务器,关闭后重启的每一秒钟都很紧迫,所以当然不能让内核白白浪费这两个MSL的时间。所以我们使用setsockopt函数,对其设置端口重用。设置之后,该端口将立即可以重用。
具体方法:
setsockopt()函数有5个参数:分别是描述符,套接字接口类型,选项名称,选项值和选项名称。
我们重点关注前三个:
第一个:描述符,就是socket时候创建的那个描述符,服务描述符。
第二个:套接字接口类型,看下面这个表格:
SOL_SOCKET | 基本套接口 |
IPPROTO_IP | IPv4套接口 |
IPPROTO_IPV6 | IPv6套接口 |
IPPROTO_TCP | TCP套接口 |
我们这里使用SOL_SOCKET参数。
第三个:选项名称。这个是重点,此参数有很多选项,我们使用参数SO_REUSEADDR,也就是端口重用(好吧翻译过来是地址重用,不过不要在意这个翻译了~)。
然后第四个和第五个,指向变量的指针和该指针的空间长度,我直接设置的NULL和1。
并发多进程
/*伪代码*/
listen(listen_fd, MAX_QUEUE);
while(1)
{
client_fd = accpet(listen_fd, client_addr, sizeof( sockaddr_in));
//客户连接进程
fi( fork() == 0)
{
close(listen_fd);
while( client_request(client_fd) );
close(client);
}
//服务器进程
else
close(client_fd);
}
当然了这种架构的缺点也很明显:每个客户连接都需要分配一个独立的进程,系统开销太大&#x