1. 有关Socket的概述和观点

PS: 全部观点摘于《TCP Socket编程》及相关网络blog

  • socket, 插座,套接字。

  • socket连接起了数字世界,形成了网络。网络编程归根结底是关于信息共享和通信的,而socket是网络编程中的必不可少的最底层的部分。

  • RPC=Remote Produce Call 是一种技术的概念名词. http也是rpc实现的方式之一,也可以通过Socket自己实现一套协议来实现,包括tcp协议也是通过socket实现的。rpc调用在于比http更底层,减少网络开支。

  • Socket本质上是 *nix系统内核API,它是用来编程的,网络编程的底层。最早是出现在1983年的BSD系统中,流传至今是因为不需要了解底层协议的情况下使用它,让我们的关注点放在两个端点间的信息的交换上,而不是处理分组和序列号。

  • 其实我们读取的每一个web页面,计算机都在调用底层socket接口。

  • 网络的两端(专业术语,实际上端点可以为pc、服务器、等),各自调用Socket api创建一个socket,并且传入双方的地址和端口就可以进行网络连接,然后进行数据交换。地址和端口号是一一对应的。

  • 把服务器端的socket想象成是一家公司的总机电话(IP),而每个部门对应着不同的端口号;公司的每个部门的电话都是唯一的电话号码+分机号(IP+PORT);这个号码是不会轻易变动的(服务器);而且如果同时 有多个客户打进来会进行占线排队等候(侦听队列)。

2. Socket(Ruby)的生命周期

require 'socket'
# 1.创建
server = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
# 2.绑定
server.bind(addr)
# 3.侦听
server.listen(128)
# 4.接收
connection, _ = server.accept
# 5.读写
connection.read
connection.write
# 6.关闭
connection.close

侦听的最大长度是 Socket::SOMAXCONN,Mac上这个值等于128。

可以用 echo wtf | nc localhost 4481 简单测试下连接。

puts connection.class # Socket
puts server.fileno # 5 文件描述符编号,用于内核跟踪当前进程打开文件的一种方法。
puts connection.fileno # 8 一切皆为文件
puts connection.local_address # <Addrinfo: 127.0.0.1:4481 TCP> 
puts connection.remote_address # <Addrinfo: 127.0.0.1:4481 TCP> 

accept调用是阻塞式的,accept操作只不过是将还未处理的连接从侦听队列中pop而已。调用accept返回一个数组,一个是建立好的连接(Socket实例对象),另一个是远程地址(Addinfo实例对象)。 而这里accept返回的socket和绑定的服务器socket不同,因为它对应着特定的远程地址,每一个连接都由一个新的socket描述,这样服务器的socket可以不用变,不停的接受新的连接。

一般ruby服务器这么干:

loop do
  connection, _ = server.accept
  connection.close
end

必须及时关闭连接,因为所有的进程只能打开一定数量文件,每个连接都是一个文件,可以通过Process.gettrlimit(:NOFILE)查看,Process.settrlimit来配置。

socket是的双向通信的(读/写),关闭的时候也可以之关闭一个。

connection.close_write # 会发送一个EOF到socket的另一端,通知写完了。
connection.close_read

close_read和close_write在底层都调用了shutdown接口,他与底层close接口不同的是,即使存在连接的副本也可以关闭。 Socket.dup可以创建连接的副本,更常见是用Process.fork,创建一个全新的进程,该进程和当前进程一模一样,除了拥有当前进程在内存中的所有内容外,新进程还通过底层的dup接口获得所有的打开了的文件描述符的副本。

copy = connection.dup # 创建连接副本
connection.shutdown # 关闭所有连接副本上的通信
connection.close # 关闭原始连接,副本会在gc时自动关闭。

3. 更ruby的写法

require 'scoket'
server = TCPServer.new 4481

等价于

require 'socket'
server = Socket.new :INET, :STREAM
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')
server.bind(addr)
server.listen(5)

4. 读写缓冲

  • 所有的读写io缓冲对性能都有大大的提升。
  • 缓冲层无处不在,Ruby内核存在,操作系统内核也存在,TCP套接字默认将sync设置为true,默认跳过ruby内部的缓冲。
  • [写]通常情况下,获得最佳性能的方法是一口气写入所有数据,让内核决定如何对数据进行组合。
  • [读]当指定读取长度,内核会为我们分配一定的内存,如果用不到那么多会造成资源浪费;但如果设定一个较小的长度,就会进行多次读取才能读取全部数据,这回引起每次系统调用已发大量开销的问题。这个问题没有万能解药。Mongrel、 Unicorn、 Puma、 Passenger 以及 Net:: HTTP, 都采用 了 readpartial( 1024* 16)。16kb
  • 然而,redis-rb使用的1kb作为读取的长度。可以适量调整以达最佳性能,16kb是默认的无二之选。

【读】

require 'socket' 
one_hundred_kb = 1024 * 100
Socket.tcp_server_loop(4481) do | connection| 
	begin # 每次 读取 100KB 或 更少。 
		while data = connection.readpartial(one_hundred_kb) do 
			puts data 
		end 
	rescue EOFError 
	end 
	connection.close 
end

【写】

require 'socket' 
Socket.tcp_server_loop(4481) do | connection| 
	# 向 连接 中写 入 数据 的 最简单 的 方法。 
	connection.write(' Welcome!') 
	connection.close 
end

5. 网络架构模式

5.1 Preforking(Unicorn)

它依赖进程作为并行操作的]手段,但并不为每个接入的连接衍生对应的子进程,而是在服务器启动后,连接到达之前就先衍生出一批进程。

下面 是 处理 流程:

  1. 主服务器进程创建一个侦听套接字;
  2. 主服务器进程衍生出一大批子进程;
  3. 每个子进程在共享套接字上接受连接,然后进行独立处理
  4. 主服务器进程随时关注子进程

这个流程的重点是,主服务器进程打开侦听套接字,却并不接受该套接字之上的连接。它然后衍生出预定义数量的一批子进程,每个子进程都有一份侦听套接字的副本。子进程在各自的侦听套接字上调用accept,不再考虑父进程。

这个模式的精妙之处在于,无须担心负载均衡或是子进程连接的同步,因为内核已经替我们完成这个工作了。对于多个进程试图在同一个套接字的不同副本上接受(accept)连接的问题,内核会均衡负载并确保只有一个套接字副本可以接受某个特定的连接。

5.2 线程池(Puma)

线程池模式之于Preforking,一如单连接线程与单连接进程之间的关系。同Preforking差不多,线程池在服务器启动后会生成一批线程,将处理连接的任务交给独立的线程来完成。这个架构的处理流程和前一个一样,只需要把“进程”改成“线程”就行了。