五:交换数据
已经建立了服务器和客户端的链接,现在需要让它们进行数据交换;
你可以将TCP连接想象成一串连接了本地套接字和远程套接字的管子,我们可以沿着这个管子发送和接受数据;
实际中,数据被编码为TCP/IP分组,经过多台路由器和主机,抵达终点;
5.1 流:
TCP具有流性质:
TCP是一个基于流的协议,这也是创建套接字时,传入的:STREAM选项的意思;
前面提到了分组,从协议层面上而言,TCP在网络上发送的就是分组;
从代码的角度来说,TCP连接提供了一个不间断的,有序的通信流;
比如,你可以在客户端发送3分数据,一次一份,在服务端一次操作(当做一份数据)就可以读取全部数据;
即,流并没有消息边界(流中内容的次序还是会保留的);
5.2 套接字读操作
介绍各种读取数据的方式以及各自的使用场景;
5.2.1 简单的读操作
从套接字读取数据的最简单方法是使用read:
# ./code/snippets/read.rb
require 'socket'
Socket.tcp_server_loop(4481) do | connection |
# 从连接中读取数据的最简单方法
puts connection.read
# 完成读取之后关闭连接,让客户端知道不用在等待数据返回
connection.close
end
服务端开始侦听4481端口,此时可以使用netcat命令,连接本地localhost 4481端口
echo gekko | nc localhost 4481
(没有运行Ruby程序,在终端做了简单测试,查看输出结果)(图1)
Ruby中所有的IO对象(套接字、管道、文件……)都有一套通用的接口,支持read,write,flush等方法;
底层的read(2)、write(2)等系统调用都可以作用于文件、套接字、管道等之上;这种抽象源自操作系统本身,记住,一切皆文件;
5.2.2 没那么简单
如果运行的是如下命令:
tail -f /var/log/system.log | nc -t 127.0.0.1 4481
然后不管了,服务器将永远不会停止读取数据;(图2)
原因是EOF(end-of-file),之后会介绍;
问题在于tail -f根本就不会停止发送数据;如果tail没有数据可以发送,他会一直等到有为止;这使得netcat的管道一直处于打开状态;
服务器的read调用就一直被阻塞着,直到客户端发送完数据为止;服务器等待的过程中,会将收到的数据缓冲起来,不返回给应用程序;
5.2.3 读取长度
作为上述问题的一个不太成熟的解决方式:指定最小的读取长度;
这样就不用等到客户端结束发送才停止读取,而是告诉服务器读取(read)特定的数据量;
# ./code/snippets/read.rb
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
然后再运行:
tail -f /var/log/system.log | nc -t 127.0.0.1 4481
会使服务器以1kb为单位打印数据;
这个例子中给read传递了一个参数:他告诉read在读取了一定数量的数据之后就停止读取并返回;
由于希望得到所有可用的数据,我们在调用read方法时使用了循环,直到它不再返回数据为止;
(在《iOS平台Socket编程实践》系列文章中,演示了相关的数据传输)
注:
请注意区分一个概念,无论是服务器还是客户端(一对多的关系)都只是Socket连接的端点,由于是一对多的关系,对应的Socket链接也将是多个,虽然iOS中,CocoaAsyncSocket把他们封装成了一个类,但请注意区分;
5.2.4 阻塞的本质
read调用会一直阻塞,知道获取了完整的长度(full length)的数据为止;
上面的例子有个问题,就是如果读取了若干次之后的数据不足1kb,那么read会一直阻塞;
如此会导致死锁,可以采用的补救措施:
1)客户端发送了500B后就再发送一个EOF;
2)服务器采用部分读取(partial read)的方式;
5.2.5 EOF事件
当调用read并受到EOF时,就意味着不再有数据,可以停止读取了;(对于理解IO很重要)
EOF代表一个状态事件,shutdown和close都会导致一个EOF事件被发送给另一端进行读操作的进程,这样他就知道不会在有数据到达了;
下面是正确的数据读取代码:
# ./code/snippets/read.rb
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
客户端连接:
# ./code/snippets/read.rb
require 'socket'
client = TCPSocket.new('localhost' , 4481)
client.write('gekko')
client.close
5.2.6 部分读取
即readpartial;
readpartial并不会阻塞,而是立刻返回可用数据;调用时,需要传入整数参数,指定最大长度(readpartial最多读取到指定长度);
指定读取1kb,但只发送500B的话,readpartial不会阻塞,而是会立刻返回已经读取到的数据;
服务端:
# ./code/snippets/readpartial_with_length.rb
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
这样,只要有数据,readpartial就会将其返回,即便是小于最大长度;
EOF而言,read仅仅是返回,readpartial则会产生一个EOFError异常,提醒我们小心;
总结就是:
read很懒惰,只会傻等,以求返回尽可能多的数据;
readpartial更勤快,只要有数据可用就立刻返回;
在学习缓冲区之后,我们会对一次性读取多少数据,小读取量多次和大读取量单次那个更好等问题做出回答;
5.3 套接字写操作
write方法:
# ./code/snippets/readpartial_with_length.rb
require 'socket'
Socket.tcp_server_loop(4481) do | connection |
# 向连接中写入数据的最简单方式
connection.write('Welcome!')
connection.close
end
5.4 涉及到的系统调用
·Socket#read->read(2) //ri Socket#read -> man 2 read
·Socket#readpartial->read(2) //ri Socket#readpartial -> man 2 read
·Socket#write->write(2) //ri Socket#write -> man 2 write
总结:
下一节将会介绍几个较高级的概念,不过到目前为止,我所不知道的Socket已经没那么多了,你的呢。