通信协议之传输层
网络通信分层
OSI是Open System Interconnection的缩写,中文翻译是开放式系统互联。国际标准化组织(ISO)制定了OSI模型,它定义了不同计算机之间实现互联的标准,是网络通信的基本框架。OSI模型将网络通信的工作分为7层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
由于复杂度过高,OSI模型并没有TCP/IP模型应用广泛。TCP/IP是Transmission Control Protocol/Internet Protocol的简写,它可以被理解为OSI模型的浓缩版本,它将原OSI七层网络模型抽象为了四层:原OSI七层网络模型中的物理层和数据链路层对应为网络接口层;原OSI七层网络模型中的网络层和传输层仍然保留;原OSI七层网络模型中的会话层、表示层和应用层则合并为统一的应用层。
TCP
Java中使用Socket开发TCP
Socket是用于连通应用层与传输层之间的抽象层接口。Socket翻译为套接字,这个翻译并不有助于理解,因此下文还是统一称其为Socket。它采用IP地址和端口的方式确定一个网络环境中唯一的通信句柄,应用通过句柄向网络中的其他服务发送请求或应答并处理所接收的请求。
Java的网络编程基础是从Socket的TCP编程开始的,TCP是C/S模式,即客户端/服务器模式,这与服务化中的消费者/提供者模式概念等同,只不过服务化中的应用可以既是服务消费者,也可以同时是其它服务的提供者。Java的Socket编程API封装了TCP的三次握手等复杂交互。
通过以下4个步骤可以简单的实现一个基于TCP的Socket编程的正常处理流程:
- 服务端程序绑定一个未占用的端口用于监听客户端程序的连接请求。
- 客户端程序向服务端发起连接请求,请求过程中附带自身的主机IP地址和通信端口号。
- 服务端接受客户端的连接请求。
- 客户端与服务端通过Socket进行IO通信。
- 建立通信管道后,可以考虑使用多线程的机制增加服务端的吞吐量。
- 完成通信,客户端断开与服务端的连接。
以下代码是基于TCP的服务端的Java语言的Socket代码示例。由两个类组成,TCPServer用于启动服务器进程和消息分发。TCPServerHandler用于处理消息和业务逻辑。
首先看一下TCPServer的代码示例:
public class SocketServer {
private static volatile boolean stopped;
private static int port = 8488;
public static void main(String [] args ) throws IOException {
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
ServerSocket serverSocket = new ServerSocket(port);
while (!stopped){
// accept方法是阻塞的,直到接收到一个连接返回一个客户端的Socket对象实例
Socket socket = serverSocket.accept();
// 采用线程池处理,提升一下性能
executorService.submit(new TcpServerHandler(socket));
}
}
}
- 使用一个变量控制服务端进程的停止。使用volatile关键字修改此变量以保证它在多线程中的可见性。
- 定义线程池来控制服务端的线程数量。代码示例中使用运行服务器的CPU核数 * 2定义线程池允许运行的最大线程数量。
- 创建ServerSocket对象并使用8899端口监听客户端的连接请求。
- 服务端使用一个永久循环来维持运行状态,在处理完一个客户端连接后阻塞,直到下一个客户端的连接到达。这里一个volatile的布尔变量,来控制服务端的优雅关闭。
- 接受客户端的请求,并完成连接的创建以及获取其输入流和输出流。为了简单起见,示例代码中并未采取多线程的方式处理请求。可以将获取连接之后的处理都放入一个新的线程,让主线程立刻返回,以便加快响应下一个客户端的连接请求。
- 使用TCPServerHandler类处理业务逻辑。TCPServerHandler是一个实现了Runnable的线程类,将其提交至线程池,以多线程的方式执行IO和具体业务逻辑。使得当前方法可以立刻返回,可以快速接受下个客户端的连接请求。
- 独立定义一个stop方法。通过改变控制服务端停止循环的标志变量的值来停止服务端进程的运行。
接下来在看一下TCPServerHandler代码示例
public class TcpServerHandler implements Runnable {
private final Socket socket;
public TcpServerHandler (final Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try{
sayHello();
} catch (IOException e){
e.printStackTrace();
}
}
private void sayHello() throws IOException {
StringBuilder message = new StringBuilder("Hello, ");
byte[] buffer = new byte[1000];
int length;
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
// 读取输入流并且返回
while ( -1 != (length = inputStream.read(buffer))){
message.append(new String(buffer, 0, length));
}
outputStream.write(message.toString().getBytes());
socket.shutdownOutput();
}
}
- 覆盖Runnable的run方法接口,用于实现多线程里面的业务逻辑。
- sayHello方法的业务逻辑是读取客户端传输来的消息,并在其消息之前插入”Hello, ”的问候话语。
- 通过try with resource机制自动释放资源。这里会在try代码块结束之后自动关闭socket、socket的输入流和socket的输出流这3个对象。
- 读取客户端发送的消息。这里使用了Java的IO API,先使用read(byte [] b)方法,读取消息,并将结果放入buffer的字节数组中。这里定义的字节数组大小是1024个字节。如果传输的消息大于这个buffer的字节数组,则需要反复读取,并在每次读取时将中间结果放入buffer。在read的返回值等于-1时,即表示客户端传输的消息已经结束,可以结束读取。如果read的返回值不为-1,则表示上次读取的字节数。因此在循环读取时,需要使用read(byte [] b, int offset, int length)方法,以避免最后一次读取消息时,并未使用全部的buffer字节数组,导致字节数组中length之后的数据仍然是上次读取的脏数据。
- 将结果信息发送至客户端。
- 关闭输出流。由于read方法是阻塞的,在返回结果不是-1时,会阻塞当前线程,继续等待读取数据。只有调用了shutdownOutput方法之后,socket即会关闭输出流,read的返回值才是-1,用于结束读取阻塞。
以下代码是基于TCP的客户端的Java语言的Socket代码示例:
public class TcpClient {
private static int port = 8488;
public static void main(String [] args ) throws IOException {
Socket socket = new Socket("localhost",port);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
System.out.println(getSayHelloMessage("Jerry", socket, inputStream, outputStream));
}
private static String getSayHelloMessage(String name, Socket socket, InputStream inputStream, OutputStream outputStream) throws IOException {
outputStream.write(name.getBytes());
socket.shutdownOutput();
StringBuilder result = new StringBuilder();
byte [] buffer = new byte[1024];
int length;
// 读取返回的流并且返回
while ( -1 != (length = inputStream.read(buffer))){
result.append(new String(buffer, 0, length));
}
return result.toString();
}
}
- 使用TCPServerHandler类处理业务逻辑。TCPServerHandler是一个实现了Runnable的线程类,将其提交至线程池,以多线程的方式执行IO和具体业务逻辑。使得当前方法可以立刻返回,可以快速接受下个客户端的连接请求。
- 调用业务方法getSayHelloMessage,向服务端发送信息,并将获取到返回结果打印到控制台。
- 在getSayHelloMessage业务方法中,发送名字到服务端。
- 关闭输出流,以示意服务端结束读取阻塞。
- 读取服务端返回的消息。由于服务端在返回消息时同样调用了shutdownOutput方法,因此客户端在读取消息结束后解除阻塞。