ZetCode

Ruby Socket

最后修改于 2023 年 10 月 18 日

在本文中,我们将介绍如何使用套接字进行 Ruby 网络编程。套接字编程是低级的。本教程的目标是介绍网络编程,包括这些底层细节。

在编程中,套接字 是在网络上运行的两个程序之间通信的端点。套接字用于在客户端程序和服务器程序之间创建连接。

Ruby 的 socket 模块提供了对 Berkeley 套接字 API 的接口。

注意: 在网络中,套接字一词有不同的含义。它用于 IP 地址和端口号的组合。

网络协议

TCP/IP 是一套协议,设备使用它通过 Internet 和大多数本地网络进行通信。TCP 更可靠,具有广泛的错误检查,需要更多的资源。HTTP、SMTP 和 FTP 等服务使用它。UDP 的可靠性差得多,错误检查有限,需要更少的资源。VoIP 等服务使用它。

Socket::SOCK_STREAM 用于创建 TCP 套接字,而 Socket::SOCK_DGRAM 用于 UDP。

地址族

创建套接字时,我们必须指定其地址族。然后,我们只能使用该类型的地址与该套接字。

对于 AF_INET 地址族,指定一个 (主机,端口) 对。主机 是一个字符串,表示 Internet 域表示法中的主机名(如 example.com)或 IPv4 地址(如 93.184.216.34),端口是一个整数。

Ruby Socket.getnameinfo

使用 Socket.getnameinfo 获取有关套接字地址的信息。

get_addr.py
#!/usr/bin/ruby

require 'socket'

p Socket.getnameinfo Socket.sockaddr_in 80, "example.com"
p Socket.getnameinfo ["AF_INET", 80, "webcode.me"]

示例打印 example.com 和 webcode.me 的 IP 地址。

require 'socket'

Ruby 套接字 API 在 socket 模块中。

p Socket.getnameinfo Socket.sockaddr_in 80, "example.com"
p Socket.getnameinfo ["AF_INET", 80, "webcode.me"]

套接字地址是通过 Socket.sockaddr_in 创建的,或者通过包含协议名称、端口和主机名的值数组创建的。

$ ./get_addr.rb
["2606:2800:220:1:248:1893:25c8:1946", "http"]
["46.101.248.126", "http"]

Socket.getifaddrs & Socket.ip_address_list

Socket.getifaddrs 返回一个接口地址数组,Socket.ip_address_list 以数组形式返回本地 IP 地址。

get_locals.py
#!/usr/bin/ruby

require 'socket'

pp Socket.getifaddrs

p '----------------------------'

pp Socket.ip_address_list

示例列出所有本地接口和 IP 地址。

Ruby UDP 套接字示例

UDP 是一种通信协议,通过网络传输独立的数据包,不保证到达,也不保证传递顺序。回显是一项使用 UDP 的服务。

回显协议是 RFC 862 中定义的 Internet 协议套件中的一项服务。回显协议可以使用 TCP 或 UDP,端口号为 7。服务器会发送回它接收到的数据的相同副本。

$ cat /etc/services | grep echo | head -4
echo            7/tcp
echo            7/udp
echo            4/ddp                   # AppleTalk Echo Protocol

端口 7 保留给回显服务。

出于安全原因,大多数情况下禁用回显服务。因此,我们在本地网络中创建自己的服务。

我们在本地网络中的另一台计算机上启动回显服务。

# apt install xinetd

我们安装了 xinetd 包。该包包含 xinetd 守护进程,它是一个 TCP 包装的超级服务,用于访问回显、FTP、IMAP 和 telnet 等部分常用网络服务。

...
# This is the udp version.
service echo
{
        disable         = no
        type            = INTERNAL
        id              = echo-dgram
        socket_type     = dgram
        protocol        = udp
        user            = root
        wait            = yes
}

/etc/xinetd.d/echo 文件中,我们将 disable 选项设置为 no。

# systemctl start xinetd

我们启动了该服务。

我们使用 ip addr 检查此机器的 IP 地址;在我们的例子中,IP 地址是 192.168.0.32。

echo_client.rb
#!/usr/bin/ruby

require 'socket'

host = '192.168.0.32'
port = 7

msg =  ARGV[0] || "hello"

s = UDPSocket.new
s.connect host, port

s.sendmsg msg + "\n"
puts s.recv 20

s.close

该示例将一条小消息发送到本地网络机器上的回显服务。消息被回显回来。

s = UDPSocket.new

使用 UDPSocket.new 创建新的 UDP 套接字。

s.connect host, port

我们连接到指定端口的主机。

s.sendmsg msg + "\n"

我们使用 sendmsg 方法将消息发送到回显服务。

puts s.recv 20

recv 方法接收消息;其参数是 maxlen,即要接收的最大字节数。

s.close

我们使用 close 关闭套接字。

$ ./echo_client.rb
hello
$ ./echo_client.rb cau
cau

我们的消息被回显给我们。

# tcpdump -i eth1 -Xn -Q  in udp port 7
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth1, link-type EN10MB (Ethernet), capture size 262144 bytes
13:16:27.330620 IP 192.168.0.20.50919 > 192.168.0.32.7: UDP, length 6
    0x0000:  4500 0022 95cb 4000 4011 237b c0a8 0014  E.."..@.@.#{....
    0x0010:  c0a8 0020 c6e7 0007 000e 7382 6865 6c6c  ..........s.hell
    0x0020:  6f0a                                     o.
13:16:28.942496 IP 192.168.0.20.57886 > 192.168.0.32.7: UDP, length 4
    0x0000:  4500 0020 970f 4000 4011 2239 c0a8 0014  E.....@.@."9....
    0x0010:  c0a8 0020 e21e 0007 000c c3bf 6361 750a  ............cau.

在启动回显服务的机器上,我们可以使用 tcpdum 工具以交互方式监视流量。

Ruby TCP 套接字示例

TCP 在通过 IP 网络通信的主机上运行的应用程序之间,提供可靠、有序且经过错误检查的字节流的传输。

$ cat /etc/services | grep qotd
qotd            17/tcp          quote

端口 17 保留给每日名言服务。

每日引语服务是一种有用的调试和测量工具。每日引语服务只是发送一条短消息,而不考虑输入。

qotd_client.py
#!/usr/bin/ruby

require 'socket'

s = TCPSocket.new 'djxmmx.net', 17
s.send '', 0

puts s.recv 350
s.close

该示例创建了一个连接到 QOTD 服务的客户端程序。

s = TCPSocket.new 'djxmmx.net', 17

使用 TCPSocket.new 创建 TCP/IP 套接字。我们提供主机名和端口号。

s.send '', 0

我们向套接字发送了一条空消息。

puts s.recv 350
s.close

我们接收输出并关闭套接字。

$ ./qotd.rb 
"When a stupid man is doing something he is ashamed of, he always declares
 that it is his duty." George Bernard Shaw (1856-1950)

Ruby Socket HTTP HEAD 请求

HEAD 请求是一种 HTTP GET 请求,但不包含消息正文。请求/响应的头部包含元数据,例如 HTTP 协议版本或内容类型。

http_head.rb
#!/usr/bin/ruby

require 'socket'

s = TCPSocket.new 'webcode.me', 80

s.write "HEAD / HTTP/1.0\r\n"
s.write "Host: webcode.me\r\n"
s.write "User-Agent: Console Http Client\r\n"
s.write "Accept: text/html\r\n"
s.write "Accept-Language: en-US\r\n"
s.write "Connection: close\r\n\r\n"

res = s.read
puts res

s.close

在示例中,我们向 webcode.me 发送了一个 HEAD 请求。

s.write "HEAD / HTTP/1.0\r\n"
s.write "Host: webcode.me\r\n"
s.write "User-Agent: Console Http Client\r\n"
s.write "Accept: text/html\r\n"
s.write "Accept-Language: en-US\r\n"
s.write "Connection: close\r\n\r\n"

HEAD 请求使用 HEAD 命令后跟资源 URL 和 HTTP 协议版本发出。请注意,\r\n 是通信过程的强制组成部分。详细信息在 RFC 7231 文档中进行了说明。

$ ./http_head.rb 
HTTP/1.1 200 OK
Server: nginx/1.6.2
Date: Tue, 02 Feb 2021 13:43:42 GMT
Content-Type: text/html
Content-Length: 348
Last-Modified: Sat, 20 Jul 2019 11:49:25 GMT
Connection: close
ETag: "5d32ffc5-15c"
Accept-Ranges: bytes

Ruby Socket GET 请求

HTTP GET 方法请求指定资源的表示。使用 GET 的请求应该只检索数据。

http_get.rb
#!/usr/bin/ruby

require 'socket'

s = TCPSocket.new 'webcode.me', 80
s.write "GET / HTTP/1.0\r\n\r\n"

res = s.read
puts res

s.close

该示例使用 GET 请求读取 webcode.me 的主页。

s.write "GET / HTTP/1.0\r\n\r\n"

我们向 Socket 写入了一个简单的 GET 请求。

$ ./http_get.rb 
HTTP/1.1 200 OK
Server: nginx/1.6.2
Date: Tue, 02 Feb 2021 13:46:24 GMT
Content-Type: text/html
Content-Length: 348
Last-Modified: Sat, 20 Jul 2019 11:49:25 GMT
Connection: close
ETag: "5d32ffc5-15c"
Access-Control-Allow-Origin: *
Accept-Ranges: bytes

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My html page</title>
</head>
<body>

    <p>
        Today is a beautiful day. We go swimming and fishing.
    </p>
    
    <p>
         Hello there. How are you?
    </p>
    
</body>
</html>

Ruby 客户端/服务器示例

在以下部分,我们将创建一个 UDP 回显客户端和服务器。

echo_server.rb
#!/usr/bin/ruby

require 'socket'

puts 'starting echo server'

Socket.udp_server_loop 4444 do |data, src|
    src.reply data
end

回显服务器使用 Socket.udp_server_loop 创建。它在端口上创建一个 UDP/IP 服务器,并为收到的每条消息调用块。服务器将消息发送回客户端。

udp_client.rb
#!/usr/bin/ruby

require 'socket'

s = UDPSocket.new

s.connect 'localhost', 4444
s.send "hello", 0

puts s.recv 50
s.close

客户端将消息发送到回显服务器。

Ruby socket 简单 Web 服务器

以下示例创建了一个简单的 Web 服务器。

web_server.rb
#!/usr/bin/ruby

require 'socket'

port = (ARGV[0] || 8080).to_i

server = TCPServer.new port

p server
puts "server started on port #{port}"

loop do 

  client = server.accept
  puts "Request: #{client.gets}"
  client.print "HTTP/1.1 200/OK\r\nContent-type: text/html\r\n\r\n"
  client.print "<html><body><h1>#{Time.now}</h1></body></html>\r\n"
  client.close
end

服务器在 HTML 输出中发送当前时间。accept 方法接受传入连接。它返回一个新的 TCPSocket 对象。

$ ./web_server.rb 
#<TCPServer:fd 8, AF_INET, 0.0.0.0, 8080>
server started on port 8080

我们启动服务器。

$ nc localhost 8080
GET /
HTTP/1.1 200/OK
Content-type: text/html

<html><body><h1>2021-02-03 10:02:19 +0100</h1></body></html>

我们使用 nc 工具测试服务器。

在本文中,我们展示了如何使用 Ruby 中的套接字创建简单的网络程序。

作者

我叫 Jan Bodnar,是一位充满热情的程序员,拥有丰富的编程经验。我从 2007 年开始撰写编程文章。至今,我已撰写了 1400 多篇文章和 8 本电子书。我在编程教学方面拥有十多年的经验。