上面已经分别介绍了ServerSocket跟Socket的工作步骤,并且从应用层往系统底层剖析其运作原理,我们清楚了他们各自的一块,现在我们将把他们结合起来,看看他们是如何通信的,并详细讨论一下他们之间相互通信的一些细节。
借助图2-3-2-4,想象一下你正在大学课室上着电脑,你跟你另外两个朋友觉得老师讲得课很菜,没必要听,于是你们仨都各自打开浏览器冲浪,刚好你们访问了同一台服务器,假如你用的是浏览器A,那么整个流程为:
① 浏览器确认目标IP跟目标端口号(http默认使用80端口),当然如果你在浏览器地址栏输入带端口的地址,那么目标端口号将是你指定的端口,另外浏览器还将组织http报文,一并将这些参数传入系统底层socket。
② 系统层socket对TCP/IP协议的操作,从前面我们已经学到,一层一层往下其实就是将报文一层一层包起来,所以这里是先用http报文组织成TCP报文,再组织成IP报文,其中socket的源端口是随机分配的(一个大于1023随机数),最后放到TCP/IP栈里,排队发送消息。
③ ServerSocket正在堵塞监听接收连接,客户端socket与之建立连接。
④ 服务器端根据客户端请求创建socket,此socket即可用于跟客户端socket进行通信。
⑤ 服务器处理相关逻辑,例如数据库操作,逻辑判断等等。
⑥ 服务器处理完,行成相应的用于客户端浏览器显示的http报文,http报文经过tcp跟ip协议包装,返回给客户端socket。
⑦ 客户端经过层层解析,把http报文传到浏览器,浏览器对http报文进行解析,最后一个漂亮整洁的网页展示在你眼前。
⑧ 释放关闭连接。
如果你们仨在同一时间访问,服务器为了能有更好地性能,为了几乎同一时间响应你们,而不是一个处理完才处理另外一个,服务器会采取多线程处理,一个线程处理一个请求。
图2-3-2-4 ServerSocket与Socket通信模型
上面的通信模型从较宏观的层面描述了socket之间的通信,而在通信过程中必然涉及数据报文的传输,接着将学习socket底层数据的传输模型。图2-3-2-5形象地展现了数据是怎样从应用层到系统底层,再到互联网传输的。首先先介绍两个FIFO队列:SendQ跟RecvQ。它们都处于系统底层,在传输过程中充当缓冲区的作用,SendQ是把应用层发送到系统底层的数据进行缓冲,然后排队发送到因特网的队列,而RecvQ则是把从因特网接收到的数据进行缓冲,等待应用层去读取的队列。具体步骤如下:
① 应用层的Socket或Serversocket把要发送的数据写入系统底层的SendQ队列。
② SendQ队列根据先进先出原则,把这些数据发往因特网,到达目标主机。(从传输层来看,可以认为这时的数据是TCP报文)
③ 如果有数据从因特网发送过来,首先会被接收到RecvQ队列进行缓冲。
④ 应用层的Socket或Serversocket会循环尝试读取RecvQ队列,如果有数据则读取到应用层。
从中可以总结,TCP协议其实就是负责将数据按顺序从SendQ与RecvQ之间互相转移。而且从我们应用层来看这个转移过程我们是没有办法控制或直接观察的。同时,由于TCP提供可靠的数据传输服务,所以任何写入SendQ队列的数据都必须要保留一份数据副本,直到连接的另外一端成功接收。另外,Socket通过输出流向SendQ写数据并不意味着数据已经被发送,他们仅仅只是到了缓冲区,就算Socket的输出流进行了flush()操作也不能保证数据马上被发送到信道。
图2-3-2-5 socket数据传输模型
那么我们能不能就认为在连接的一端写入数据,另外一端就能马上读取数据呢?接着我们将更深入探讨SendQ与RecvQ队列里面的数据是怎样传输的,这个过程应该说是以块来传输的,而这些块的大小在一定程度上独立于应用层socket读写的缓冲大小。
如图2-3-2-6,假如应用层socket调用输出流write()方法向SendQ队列写数据,三次写入的数据大小分别为1000字节、2000字节、5000字节,而假设SendQ队列的缓存块大小为3000字节,第一次1000字节加上2000字节刚好等于3000字节,那么这两次的数据作为一个块进行传输。通过因特网到达RecvQ队列,应用层socket再通过输入流的read()方法读取RecvQ里的数据,输入流缓冲区读取了RecvQ队列3000字节就返回值3000。
图2-3-2-6 socket底层的块传输
在现实使用中,SendQ跟RecvQ队列可能会被无限填充, 为了防止一个TCP连接将系统内存全部耗尽,必须要对SendQ跟RecvQ队列的大小进行限制。一旦RecvQ队列数达到最大值,TCP流控制就会通知SendQ先停止发送数据,等我的RecvQ中的数据被socket输入流读走了有空间了再传给我。这样就能有效地控制过量发送,有效杜绝超出系统接收处理能力的情况。对于socket输出流可以不断写入数据,直到SendQ队列被填满了,此时write()方法将会阻塞等待。
喜欢研究java的同学可以交个朋友,下面是本人的微信号: