《TCP Sockets 编程》读书笔记

第一章:建立套接字

1.1 ruby 的套接字库

socket 库是 ruby 标准库的组成,包含各种用于 TCP 套接字、UDP 套接字的类

1.2 创建首个套接字

require 'socket'
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)

1.3 什么是端点

1.4 环回地址

1.5 IPv6

1.6 端口

套接字的 IP 地址和端口号的组合必须是唯一,端口号就是套接字端点的 “分机号”。

1.7 创建第二个套接字

IPv6 域中的套接字:

# ./code/snippets/create_socket_memoized.rb

require 'socket'
socket = Socket.new(:INET6, :STREAM)

1.8 系统调用

Socket.new -> socket(2)


第二章:建立连接


第三章:服务器套接字生命周期

用于侦听连接而非发起连接,其典型的生命周期如下:

  1. 创建
  2. 绑定
  3. 侦听
  4. 接受
  5. 关闭

3.1 服务器绑定

require 'socket'
# 首先创建一个新的 TCP 套接字
socket = Socket.new(:INET, :STREAM)

# 创建一个 C 结构体来保存用于侦听的地址。
addr = Socket.pack_sockaddr_in(4481, '0.0.0.0')

#执行绑定
socket.bingd(addr)

这个套接字已绑定到本机的 4481 端口,其它套接字不可用此端口,否则会产生异常 Errno::EADDRINUSE

3.1.1 该绑定到哪个端口

3.1.2 该绑定到哪个地址

3.2 服务器侦听

创建套接字绑定到端口之后,需要进行侦听.

require 'socket'

# 创建套接字并绑定到端口 4481
socket = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockadd_in(4481, '0.0.0.0')
socket.bind(addr)

#告诉套接字侦听接入的连接
socket.listen(5)

3.2.1 侦听队列

3.2.2 侦听队列的长度

3.3 接受连接

require 'socket'
# 创建套接字并绑定到端口 4481
socket = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockadd_in(4481, '0.0.0.0')
socket.bind(addr)

#告诉套接字侦听接入的连接
socket.listen(5)

#接受连接
connection, - = server.accept

用 netcat 发起一个连接, 运行后 nc 和 ruby 程序都会顺利退出

$ echo ohai | nc localhost 4481

accept

Addrinfo

连接详解

require 'socket'
# 创建套接字并绑定到端口 4481
socket = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockadd_in(4481, '0.0.0.0')
socket.bind(addr)

#告诉套接字侦听接入的连接
socket.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 # accept 第二个返回值相同

使用 netcat 命令发起连接后会输出:

Connection class: Socket
Server fileno: 7
Connection fileno: 8
Local address: #<Addrinfo: 127.0.0.1:4481 TCP>
Remote address: #<Addrinfo: 127.0.0.1:50488 TCP>
Addrinfo"#<Addrinfo: 127.0.0.1:50488 TCP>"

3.3.3 连接类

3.3.4 文件描述符

3.3.5 连接地址

3.3.6 accept 循环

accept 返回一个连接

require 'socket'
# 创建套接字并绑定到端口 4481
socket = Socket.new(:INET, :STREAM)
addr = Socket.pack_sockadd_in(4481, '0.0.0.0')
socket.bind(addr)

#告诉套接字侦听接入的连接
socket.listen(128)

#进入无限循环,接受并处理连接
loop do
  #接受连接
  connection, - = server.accept # 返回连接,第二个 - 是远程地址
  # 处理连接
  connection.close
end

3.4 关闭服务器

服务器接受某个连接并处理完毕,那么最后需要关闭该连接。这样才算完成一个连接的 “创建-处理-关闭” 的生命周期。

3.4.1 退出时关闭

关闭连接的理由:

Process.getrlimit(:NOFILE),获知当前进程所需要打开文件的数量。 返回值是数组,包含软限制(用户配置的设置)和硬限制(系统限制)。 Process.setrlimit(Process.getrlimit(:NOFILE)[1]) 将进程的打开文件限制改为最大值

3.4.2 不同的关闭方式

套接字允许双向通信(读/写),实际上可以只关闭其中一个通道。

3.5 Ruby 包装器

3.5.1 服务器创建

require 'socket'
server = TCPServer.new(4481)

简便的服务器创建方式

require 'socket'
servers = Socket.tcp_server_sockets(4481)

以上代码同时返回两个套接字,IPv4 和 IPv6

3.5.2 连接处理

accept_loop 无限循环,并且还可以接受多个侦听套接字

require 'socket'

# 创建侦听套接字
server = TCPServer.new(4481)

# 进入无线循环接手并处理连接
Socket.accept_loop(server) do |connection|
  # 处理连接
  connection.close
end

多个套接字处理

# 创建侦听套接字
servers = Socket.tcp_server_sockets(4481) # 创建两个套接字 IPv4 和 IPv6 

# 进入无线循环接手并处理连接
Socket.accept_loop(servers) do |connection|
  # 处理连接
  connection.close
end

3.5.3 合而为一

require 'socket'

Socket.tcp_server_loop(4481) do |connection|
  # 处理连接
  connection.close
end

3.6 系统调用

Socket#bind -> bind(2)
Socket#listen -> listen(2)
Socket#accept -> accept(2)
Socket#local_address -> getsockname(2)
Socket#remote_address -> getpeername(2)
Socket#close -> close(2)
Socket#close_write -> shutdown(2)
Socket#shutdown -> shutdown(2)

第四章:客户端生命周期

网络连接两部分:

客户端的生命周期

4.1 客户端绑定

4.2 客户端连接

require 'socket'
socket = Socket.new(:INET, :STREAM)

# 发起到 baidu.com 端口 80 的连接
remote_addr = Socket.pack_sockaddr_in(80, 'baidu.com')
socket.connect(remote_addr)

4.3 ruby 包装器

客户端创建简写版本:

require 'socket'
socket = TCPSocket.new('baidu.com', 80)

代码块:

require 'socket'

Socket.tcp('baidu.com', 80) do |connection|
  connection.write "GET / HTTP/1.1\r\n"
  connection.close
end

省略代码块:

require 'socket'

client = Socket.tcp('baidu.com', 80)

4.4 系统调用

Socket#bind -> bind(2)
Socket#connect -> connect(2)

第五章:交换数据

演示伪代码:

# 下面的代码会在网络上发送 3 份数据,一次一份
data = ['a','b','c']

for piece in data
  write_to_connection(piece)
end

# 下面的代码在一次做作中读取全部数据
result = read_from_connection #=> ['a','b','c']

流并没有消息边界的概念


第六章:套接字读取

学习如何在套接字上传送数据

6.1 简单的读操作

服务端:

require 'socket'

Socket.tcp_server_loop(4481) do |connection|
  # 从连接中读取数据最简单的方法
  puts connection.read

  # 完成读取之后关闭连接,让客户端知道不用再等待数据返回
  connection.close
end

客户端调用:

echo ohi | nc lcoalhost 4481

6.2 没那么简单

客户端:

 tail -f /var/log/system.log | nc -v localhost 4481

6.3 读取长度

require 'socket'
one_kb = 1024 # 字节数

Socket.tcp_server_loop(4481) do |connection|

  # 以 1kb 为单位进行读取
  while data = connection.read(one_kb) do 
    puts data
  end

  # 完成读取之后关闭连接,让客户端知道不用再等待数据返回
  connection.close
end

6.4 阻塞的本质

解决 read 死锁的办法:

  1. 客户端发完 500B 后再发送一个 ·EOF·
  2. 服务器采用部分读取 (partial read) 的方式

6.5 EOF 事件

# ./code/snippets/read_with_length.rb

require 'socket'
one_hundred_kb = 1024 * 100 # 字节数

Socket.tcp_server_loop(4481) do |connection|
  begin
    # 以 1kb 为单位进行读取
    while data = connection.readpartial(one_hundred_kb) do
        puts data
      end
    rescue EOFError
    end

    # 完成读取之后关闭连接,让客户端知道不用再等待数据返回
    connection.close
  end

客户端连接:

# ./code/snippets/write_with_eof.rb

require 'socket'
client = TCPSocket.new('localhost', 4481)
client.write('gekko')
client.close

6.6 部分读取

# ./code/snippets/readpartial_with_length.rb

require 'socket'
one_hundred_kb = 1024 * 100 # 字节数

Socket.tcp_server_loop(4481) do |connection|
  begin
    # 以 1kb 为单位进行读取
    while data = connection.readpartial(one_hundred_kb) do
        puts data
      end
    rescue EOFError
    end

    # 完成读取之后关闭连接,让客户端知道不用再等待数据返回
    connection.close
  end

6.7 系统调用

Socket#read -> read(2),行为类似 fread(3)
Socket#readpartial -> read(2)

第七章:套接字写操作

require 'socket'

Socket.tcp_server_loop(4481) do |connection|

  # 向连接中写入数据的最简单的方法
  connection.write('Welcome!')
  connection.close
end

第八章:缓冲

8.1 写缓冲

8.2 读写入多少数据

8.3 读缓冲

8.4 该读取多少数据


第 10 章:套接字选项

10.1 SO_TYPE

require 'socket'
socket = TCPSocket.new('google.com', 80)

# 获得一个描述套接字类型的 Socket::Option 实例
opt = socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_TYPE)

# 将描述该选项的整数值同存储在 Socket::SOCK_STREAM 中的整数值进行比较
puts opt.int == Socket::SOCK_STREAM #=> true
puts opt.int == Socket::SOCK_DGRAM #=> false

简便方式:

require 'socket'
socket = TCPSocket.new('google.com', 80)

# 使用符号名,而不是常量
opt = socket.getsockopt(:SOCKET, :TYPE)

10.2 SO_REUSE_ADDR

示例代码:

require 'socket'
server = TCPServer.new('localhost', 4481)
server.setsockopt(:SOCKET, :REUSEADDR, true)

server.getsockopt(:SOCKET, :REUSEADDR) #=> true

10.3 系统调用

Socket#setsockopt -> setsockopt(2)
Socket#getsockopt -> getsockopt(2)

第11章:非阻塞式 IO

11.1 非阻塞式读操作

两种阻塞的读操作

**Socket#read_nonblock **:

read_nonblock 方法首先检查 ruby 的内部缓冲区中是否还有未处理的数据,如果有,则立即返回 read_nonblock 会询问内核是否有其他可用的数据可供 select(2) 读取,如果有,不管这些数据是在内核缓冲区还是网络中,他们都会被读取并返回 其他情况都会使 read(2) 阻塞并在 read_nonblock 中引发异常

require 'socket'

Socket.tcp_server_loop(4481) do |connection|
  begin
    puts connection.read_nonblock(4096)
  rescue Errno::EAGAIN => e
    IO.select([connection])
    retry
  rescue EOFError
    break
  end

  connection.close
end

11.2 非阻塞式写操作

./code/snippets/write_nonblock.rb
require 'socket'

client = TCPSocket.new('localhost', 4481)
payload = 'Lorem ipsum' * 100_000

written = client.write_nonblock(payload)
puts written < payload.size 

非阻塞,多次写入:

./code/snippets/retry_partial_write.rb

require 'socket'

client = TCPSocket.new('localhost', 4481)
payload = 'Lorem ipsum' * 100_000

begin
  loop do
    bytes = client.write_nonblock(payload)

    break if bytes >= payload.size
    puts "----#{bytes}"
    payload.slice!(0, bytes) # 删除已经写入的数据
    IO.select(nil, [client])
  end

rescue Errno::EAGAIN
  IO.select(nil, [client])
  retry
end

11.3 非拥塞式接收

11.4 非拥塞式连接


第 12 章:连接复用

示例代码:./code/snippets/native_multiplexing.rb

12.1 select(2)

# snippets/select_returns.rb
for_reading = [<TCPSocket>, <TCPSocket>, <TCPSocket>]
for_writing = [<TCPSocket>, <TCPSocket>, <TCPSocket>]

ready = IO.select(for_reading, for_writing, for_writing)

# 对于每个座位参数传入的数组均会返回一个数组
# 在这里 for_writing 中没有连接可写for_reading 中有一个连接可读
p ready #=> [[<TCPSocket>], [], []]
# snippets/select_timeout.rb
for_reading = [<TCPSocket>, <TCPSocket>, <TCPSocket>]
for_writing = [<TCPSocket>, <TCPSocket>, <TCPSocket>]

timeout = 10
ready = IO.select(for_reading, for_writing, for_writing, timeout)

# 在这里 `IO.select`  10 秒钟内没有检测到任何状态的改变
# 因此返回 nil, 而非嵌套数组
p ready #=> nil

12.2 读/写之外的事件

IO.select 监视套接字的读写状态

12.2.1 EOF

EOFend of file ,如果在监视可读性时,接到 EOF ,该套接字会作为可读套接字数组的一部分被返回

12.2.2 accept

12.2.2 connect

12.2.3 高性能复用


第 13 章:Nagle 算法

server.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)

第 14 章:消息划分

协议与消息

14.1 使用新行

14.2 使用内容长度

划分指定内容长度(content length):

代码详细见 cloudhash/server2.rb cloudhash/client2.rb


第 15 章: 超时

如果套接字没能在 5 秒内完成数据写入,那就说明存在问题

15.1 不可用的选项

15.2 IO.select

代码见 snippet/read_timeout.rb

require 'socket'
require 'timeout'

timeout = 5 # 秒

Socket.tcp_server_loop(4481) do |connection|

  begin
    # 发起一个初始化 read(2)。这一点很重要
    # 因为要求套接字上有被请求的数据,有数据可读时避免使用 select(2)
    connection.read_nonblock(4096)

  rescue Errno::EAGAIN
    # 监视连接是否可读
    if IO.select([connection], nil, nil, timeout)
      # IO.select 会将套接字返回,不过我们并不关心返回值
      # 不返回 nil 就意味着套接字可读
      retry
    else
      raise Timeout::Error  # 使用 timeout 只是为了用 Timeout::Error 常量
    end

  end

  connection.close
end

第 16 章: DNS 查询

MRI 和 GIL

resolv

require `resolv` # 库
reequire `resolv-replace` # 猴子补丁

第 17 章: SSL 套接字

第 18 章: SSL 套接字

发送紧急数据

require 'socket'

socket = TCPSocket.new 'localhost', 4481

# 会用标准方法发送数据
socket.write 'first'
socket.write 'second'

# 发送紧急数据
socket.send '!',Socket::MSG_OOB

接收紧急数据

require 'socket'

Socket.tcp_server_loop(4481) do |connection|

  # 优先接收紧急数据
  urgent_data = connection.recv(1, Socket::MSG_OOB)
  data = connection.readpartial(1024)
end

局限

紧急数据和 IO.select

SO_OOBINLINE 选项

SO_OOBINLINE 套接字选项,允许在带内接收带外数据,启用后回一句写入次序从队列读出

TCP Sockets 编程(20): 串行化

串行化架构处理流程:

  1. 客户端连接
  2. 客户端/服务器交换请求及响应
  3. 客户端断开连接
  4. 返回到步骤(1)

串行化的特点:简单化,没有锁,没有共享状态,处理完一个连接之后才能处理另一个,不能支持并发操作

TCP Sockets 编程(21): 单连接进程

单连接进程事件流程:

  1. 一个连接抵达服务器
  2. 主服务器进程接受该练级
  3. 衍生出一个和服务器一模一样的新子进程
  4. 服务器进程返回步骤 1,由子进程并行处理连接

优点:

缺点:

TCP Sockets 编程(22): 单连接线程

线程与进程:

使用线程注意:

TCP Sockets 编程(23): Preforking

Preforking 处理流程:

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

Preforking 优点:

缺点:

TCP Sockets 编程(24): 线程池