三:服务器生命周期
服务器套接字用于侦听连接而非发起连接,其典型的生命周期如下:
1)创建;
2)绑定;
3)侦听;
4)接受;
5)关闭;
创建已经在第一节中介绍完了,继续其余部分;
3.1 服务器绑定
服务器生命周期中的第二步是绑定到监听连接的端口上;
# ./code/snippets/bind.rb
require 'socket'
# 首先创建一个新的TCP套接字
socket = Socket.new(:INET, :STREAM)
# 创建一个C结构体来保存用于侦听的地址
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
# 执行绑定
socket.bind(addr)
上面是一段Ruby代码,我们需要知道的就是Socket在创建之后,需要设置监听端口号和地址;
这个套接字现在被绑定到了本地主机的端口4481上;其他套接字便不能再使用此端口(客户端套接字可以使用该端口号连接服务器套接字,并建立连接);
上述代码运行会推出,代码没错,但还不足以侦听某个链接;go on;
注:
服务器需要绑定到某个特定的,双向商定的端口号上,客户端套接字随后会连接到该端口;
3.1.1 该绑定到哪个端口
应该选择随机端口吗?该如何知道是否已经有其他的程序将某个端口宣为己有?
任何在0~65535之间的端口都可以使用,但有一些约定:
1)不要使用0~1024之间的端口:保留给系统了,如http默认使用端口80,SMTP默认使用端口25;绑定到这些端口通常需要root权限;
2)不要使用49000~65535之间的端口,这些都是临时(ephemeral)端口;通常是那些不需要运行在预定义端口,而只是需要一些端口作为临时之需的服务使用;
他们也是后面讲的‘链接协商’(connection negotiation)过程的一部分;选择该范围内的端口可能会对一些用户造成麻烦;
3)1025~48999之间端口的使用一视同仁;
(如果你打算选用其中一个作为服务端端口,应该看一下IANA的注册端口列表,确保你的选择不会和其他流行的服务器冲突;)
3.1.2 该绑定到那个地址
之前提到过系统中又一个127.0.0.1的环回接口;同时还会有一个物理的、基于硬件的接口,使用不同的IP地址(假设是192.168.0.5);当你绑定到某个由IP地址所描述的特定接口时,套接字就只会在该接口上进行侦听。而忽略其他接口;
如果绑定早127.0.0.1,那么你的套接字就只会监听环回接口;此时,只有到localhost或127.0.0.1的连接才会被服务器套接字接受;环回接口仅限于本地连接使用,无法用于外部连接;
若是其他合规地址,那么任何寻址到这个接口的客户端都在侦听范围内;
如果你希望侦听每一个接口,那么可以使用0.0.0.0;这样就会绑定到所有可用的接口,环回接口等;
4481 127.0.0.1 # 该套接字将会绑定在环回接口,只侦听来自本地主机的客户端;
4481 0.0.0.0 # 该套接字将会绑定在所有已知的接口,侦听所有向其发送信息的客户端;
4481 1.2.3.4 # 该套接字试图绑定一个未知接口,结果导致Errno::EADDRNOTAVAIL;
3.2 服务器侦听
创建套接字并绑定到特定端口之后,需要告诉套接字对接入的链接进行侦听;
# ./code/snippets/listen.rb
require 'socket'
# 创建套接字并绑定到端口4481
socket = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
socket.bind(addr)
# 告诉套接字侦听接入的连接
socket.listen(5)
运行之后,仍然会退出,因为在服务器套接字能够处理连接之前,还需要另一个步骤,之后会讲,这里先解释下listen;
3.2.1 侦听队列
我们注意到listen的方法传递一个整数参数:这个数字表示服务器套接字能够容纳的待处理(pending)的最大连接数;
待处理的连接列表被称为侦听队列;
假设服务器正忙于处理某个客户端连接,此时新的客户端连接到达,将会被置于侦听队列;如果新的客户端连接到达且侦听队列已满,那么客户端将会产生Errno::ECONNREFUSED;
3.2.2 侦听队列的长度
为啥不把这个长度设成10000或是比较大的数?
侦听队列长度的限制:通过运行时查看Socket::SOMAXCONN可以获知当前所允许的最大侦听队列长度;比如你没法使用超过128的数;
root用户可以在有需要的服务器上增加这个系统级别的限制;
如果服务器收到了Errno::ECONNREFUSED,增加队列长度,或需要更多的服务器实例,再或者需要采用其他架构;
你可以使用 server.listen(Socket::SOMAXCONN) 将侦听队列长度设置为允许的最大值;
3.3 接受连接
下面看服务器实际处理接入连接的环节;通过accept方法实现:
# ./code/snippets/listen.rb
require 'socket'
# 创建套接字
server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
server.bind(addr)
server.listen(128)
# 接受连接
connection , _ = server.accept
运行的话,没有退出:因为accept方法会一直阻塞到有连接到达;
可以使用netcat发起一个连接:
$ echo ohai | nc localhost 4481
运行顺利的退出的话,说明连接已经建立;
3.3.1 以阻塞方式接受连接
accept是阻塞式的;
accept会将侦听队列中还未处理的链接从队列中弹出(pop)而已;如果队列为空,那么他就一直等,直到有连接被加入队列为止;
3.3.2 accept调用返回一个数组
上面例子,accept调用获得两个返回值;
accept方法实际上返回的是一个数组,这个数组包含两个元素:第一个元素是建立好的连接,第二个元素是一个Addrinfo对象(该对象描述了客户端连接的远程地址);
Addrinfo:
是一个Ruby类,描述了一台主机及其端口号,它将端点信息进行了包装;
可以使用Addrinfo.tcp('localhost',4481)构建这些信息;一些有用的方法包括#ip_address,#ip_port;
查看$ri Addrinfo:(图)
接下来仔细查看一下#accept返回的连接和地址:
# ./code/snippets/accept_connection_class.rb
require 'socket'
# 创建服务器套接字
server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
server.bind(addr)
server.listen(128)
#接受一个新连接
connection , _ = server.accept
print 'Connection class:'
p connection.class
print 'Server fileno:'
p server.fileno
print 'Connection fileno:'
p connection.fileno
print 'Local address:'
p connection.local_address
print 'Remote address:'
p connection.remote_address
当服务器获得一个连接(nc -t localhost 4481,使用该命令创建一个tcp连接),上述代码会输出:
Connection class: Socket
Server fileno: 5
Connection fileno: 8
Local address: #<Addrinfo:127.0.0.1:4481 TCP>
Remote address: #<Addrinfo:127.0.0.1:5816 4 TCP>
下面逐个分析;
3.3.3 连接类
accept返回的第一个参数:
尽管accept返回了一个‘连接’,但上述代码告诉我们并没有特殊的连接类,一个连接实际上就是Socket的一个实例;
3.3.4 文件描述符
知道了accept返回一个Socket的实例,不过这个连接的文件描述符编号和服务器套接字不一样;
文件描述符是内核用于跟踪当前进程所打开文件的一种方法;-套接字是文件(Unix世界中,一切都是被视为文件);
这表明accept返回了一个不同于服务器套接字的全新Socket;
这个Socket实例描述了特定的连接(每个连接都由一个全新的Socket对象描述),这样服务器套接字就可以保持不变,不停地接受新的连接;
3.3.5 连接地址
accept返回的第二个参数:
连接对象知道两个地址:本地地址和远程地址,其中远程地址也是accept的第二个返回值,不过也可以从连接中remote_address访问到;
连接的local_address指的是本地主机的端点;
每一个TCP连接都是由“本地主机 本地端口 远程主机 远程端口”这组唯一的组合定义的;
3.3.6 accept循环
accept会返回一个循环;
前面的例子中服务端接受一个连接之后退出,我们希望只要有接入的连接,就不停地侦听;这个可以通过循环来实现;
# ./code/snippets/accept_connection_class.rb
require 'socket'
# 创建服务器套接字
server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
server.bind(addr)
server.listen(128)
# 进入无限循环,接受并处理连接
loop do
connection , _ = server.accept
#处理链接
connection.close
end
Ruby也提供了一些语法糖来简化这些语法;
3.4 关闭服务器
一旦服务器接受了某个连接并处理完毕,最后就是关闭该连接;
这就算完成了一个连接的“创建-处理-关闭”的生命周期;
3.4.1 退出时关闭
程序退出时,系统会帮你关闭所有打开的文件描述符(包括套接字),那为什么还需要手动关闭呢?
1)资源使用:
如果你使用了套接字却没有关闭它,那么不再使用的套接字使用可能依然保留;(在Ruby中,垃圾收集器会清理用不着的连接,将它收集到的所有一切全部关闭)
2)打开文件的数量限制:
所有进程都只能打开一定数量的文件(每一个连接都是一个文件);
获知当前进程所允许打开文件的数量,可以使用Process.getrlimit(: NOFILE)
返回值是一个数组,包含了软限制(用户配置的设置)和硬限制(系统限制);
如果想将限制设置为最大值,可以使用Process.setrlimit(Process.getrlimit(: NOFILE)[1])
3.4.2 不同的关闭方式
因为套接字允许双向通信(读/写),可以只关闭其中一个通道;
# ./code/snippets/accept_connection_class.rb
require 'socket'
# 创建服务器套接字
server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
server.bind(addr)
server.listen(128)
connection , _ = server.accept
# 该连接随后也许不再需要写入数据,但可能仍然需要进行读取
connection.close_write
# 该连接不再需要进行任何数据读写操作
connection.close_read
关闭写操作流(write stream)会发送一个EOF到套接字的另一端(很快会讲到EOF);
close_wirte和close_read方法在底层都用到了shutdown(2),与close(2)明显不同的是:即便是存在着连接的副本,shutdown(2)也可以完全关闭该连接的某一部分;
注:连接副本是怎么回事?
可以使用Socket#dup创建文件描述符的副本(实际是在os层面上利用dup(2)复制了底层的文件描述符);
获得一个文件描述符副本的常用方法是用Process.fork方法,创建一个和当前进程一模一样的全新进程(会获得当前进程所有已经打开的文件描述符副本以及内存中的所有内容);
close会关闭调用它的套接字实例;(不会关闭它的副本)
shutdown会完全关闭当前套接字及其副本上的通信;(但是他并不回收套接字所使用过的资源,每个套接字实例必须使用close结束他的生命周期)
# ./code/snippets/accept_connection_class.rb
require 'socket'
# 创建服务器套接字
server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
server.bind(addr)
server.listen(128)
connection , _ = server.accept
# 创建连接副本
copy = connection,dup
# 关闭所有连接副本上的通信
connection.shutdown
# 关闭原始连接(副本会在垃圾收集器进行收集时关闭)
connection.close
3.5 Ruby包装器
本节原是介绍使用Ruby来创建及使用服务器套接字的扩展的内容,便捷的将样本代码(之前的那些)包装到定制的类中并尽可能的使用Ruby语句块;
这里仅仅给出一端示例,想深入了解的同学,请自行学习Ruby(看看示例才发现,Ruby如此优雅):
# ./code/snippets/tcp_sercer_loop.rb
require 'socket'
Socket.tcp_server_loop(4481) do | connection |
#处理链接
connection.close
end
稍作解释:创建和循环处理Sockets链接;
端点是 4481 0.0.0.0;
Sockets 分别是支持IPv4和IPv6的socket连接server(注意他不是Socket实例了,而是TCPServer实例,一个Ruby包装类);
队列长度也被设置为默认了,可以调用TCPServer#listen设置;
循环处理就比较明显了;
3.6 本章涉及的系统调用:
(每一章都会列出新介绍的系统调用,告诉你如何使用ri或手册页来获得更多的信息)
·Socket#bind->bind(2) //ri Socket#bind -> man 2 bind
·Socket#listen->listen(2) //ri Socket#listen -> man 2 listen
·Socket#accept->accept(2) //ri Socket#accept -> man 2 accept
·Socket#local_address->getsockname(2) //ri Socket#local_address -> man 2 getsockname
·Socket#remote_address->getpeername(2) //ri Socket#remote_address -> man 2 getpeername
·Socket#close->close(2) //ri Socket#close -> man 2 close
·Socket#close_write->shutdown(2) //ri Socket#close_write -> man 2 shutdown
·Socket#shutdown->shutdown(2) //ri Socket#shutdown -> man 2 shutdown
总结
服务器端就这些,下一节介绍客户端的生命周期。