
图片来源:Pexels
这里提供了IO和NIO场景下的完整源代码,你可以在这里找到。
在客户端服务器应用中,当客户端向服务器发出请求时,服务器会处理请求并返回响应。为了实现这一点,客户端和服务器都需要首先建立连接,这就是套接字的作用。客户端和服务器都必须将自己绑定到连接的套接字上,而服务器则在其套接字上等待客户端发出连接请求。

因此,当客户端和服务器之间建立连接时,它们都从绑定到该连接的套接字中读取和写入数据。(TCP层如何识别将数据发送到哪个应用程序是通过服务器的端口号来绑定套接字来实现的。)
注:I/O在计算机世界中是指输入/输出,用于表示系统之间的通信方式。
阻塞I/O
使用阻塞I/O时,当客户端请求连接到服务器时,处理该连接的线程会被阻塞,直到有数据可读取或数据完全写入为止。在相关操作完成之前,该线程除了等待之外无法执行其他任何操作。现在为了使用这种方法满足并发请求,我们需要有多个线程,也就是为每个客户端连接分配一个新线程。让我们通过一个简单的代码片段来了解这一点。
- 在这里,我们创建了一个新的服务器套接字,以便在特定端口上侦听请求连接,现在服务器已绑定到此端口。
ServerSocket serverSocket = new ServerSocket(portNumber);

服务器套接字监听客户端连接
- 接下来,当我们调用accept()方法时,服务器开始等待客户端建立连接,当客户端发出请求时,服务器套接字接受来自客户端的连接并返回一个新的套接字以与客户端通信。在建立此新连接之前,服务器套接字被阻塞,但一旦建立连接,它将返回原始服务器套接字上的客户端连接以等待客户端连接。
Socket clientSocket = serverSocket.accept();


- 接下来,我们可以从套接字获取输入和输出流。
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
- 然后我们从输入流中读取一行信息,处理客户端发送的内容并将响应写入附加到套接字的输出流。这将一直发生,直到客户端发送“完成”或输入结束字符(通过按下Ctrl-C)为止。
String request, response;
while ((request = in.readLine()) != null) {
response = processRequest(request);
out.println(response);
if ("Done".equals(request)) {
break;
}
}
现在在这里需要记住的重要事情是,此代码仅适用于一次处理一个连接。为了处理多个并发用户,我们需要为每个客户端套接字分配一个新线程。
while (listening) {
accept a connection;
create a thread to deal with the client;
}
这个阻塞I/O场景下的完整客户端和服务器代码可以在github中找到。

每个连接一个线程
这种方法存在一些缺点。
- 每个线程都需要分配一块内存栈,随着连接数量的增加,产生多个线程并在它们之间切换将变得麻烦。
- 在任何给定时间点上,可能会有多个线程只是在等待客户端请求,这只是资源的浪费。
因此,如果你需要处理大量客户端,则此阻塞I/O方法不是理想的,但对于少量到中等数量的客户端来说,这种方法仍然很好。
但是当我们需要支持大量并发连接时,我们该怎么办呢?幸运的是,有一种替代方法。
非阻塞I/O
使用非阻塞I/O,我们可以使用单个线程处理多个并发连接。在我们进入细节之前,有一些术语需要先了解。
- 在基于NIO的系统中,我们不是将数据写入输出流并从输入流中读取数据,而是从“缓冲区”中读取和写入数据。你可以将“缓冲区”视为临时存储位置,并且有不同类型的Java NIO缓冲区类(例如:ByteBuffer,CharBuffer,ShortBuffer等)可供我们使用,尽管大多数网络程序仅使用ByteBuffer。
- “通道”是将大量数据传输到缓冲区和从缓冲区中传输出来的媒介,它可以被视为通信的端点。(例如,如果我们使用“SocketChannel”类,则它从TCP套接字读取和写入数据。但是,必须将数据编码为ByteBuffer对象进行读取和写入。)
- 然后我们需要了解一个称为“就绪选择”的概念,它基本上意味着“在读取或写入数据时不会阻塞的套接字的选择能力”。让我们进一步探讨一下。
Java NIO有一个名为“Selector”的类,允许单个线程检查多个通道上的I/O事件。也就是说,此选择器可以检查通道的准备就绪情况,例如读取和写入。现在请记住,不同的通道可以向“Selector”对象注册,并且你可以指定你感兴趣观察的操作,还有一件需要记住的事情是,这些通道中的每个都分配了一个单独的“SelectionKey”,它作为指向通道的指针。现在是时候创建一个基于NIO的简单客户端和服务器了。
首先让我们看一下服务器代码。
- 首先,我们需要创建一个选择器来处理多个通道,更重要的是,它们允许服务器找到所有准备接收输出或发送输入的连接。
Selector selector = Selector.open();
- 现在,让我们以非阻塞的方式创建一个服务器套接字通道,这个“ServerSocketChannel”类完全负责接受新的传入连接。
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
- 然后,我们可以将服务器套接字通道绑定到特定的主机和端口。
InetSocketAddress hostAddress = new InetSocketAddress(hostname, portNumber);serverChannel.bind(hostAddress);
- 现在,我们需要向选择器注册这个服务器套接字通道,而“SelectionKey.OP_ACCEPT”参数告诉“selector”仅监听传入的连接。基本上,第二个参数告诉我们我们有兴趣监听被监视通道的哪些事件。在我们的例子中,“OP_ACCEPT”表示服务器套接字通道准备从客户端接受新连接。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
- 接下来,我们将调用选择器的select()方法来检查是否有任何准备好执行的操作。如果你想无限期地等待新的活动,请在无限循环中调用它。 “readyCount”表示准备好的通道数。如果我们没有准备好的通道,我们可以继续等待。
while (true) {
int readyCount = selector.select();
if (readyCount == 0) {
continue;
} // process selected keys...
}
- 一旦选择器找到准备好的通道,“selectedKeys()”方法返回一组“readyKeys”,每个键代表一个准备好的通道,我们可以循环遍历每个通道并执行必要的操作。
// process selected keys...
Set<SelectionKey> readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// Remove key from set so we don't process it twice
iterator.remove();
// operate on the channel...
}
- 这里需要注意的重要事情是,只有一个线程,即主线程,处理多个同时连接。
- 接下来,让我们看看当我们获得一个通道时,如何处理操作逻辑。由SelectionKey表示的通道可以是通知新连接已经建立的服务器套接字,也可以是准备从通道中读取或写入数据的客户端套接字。
- 如果键是“可接受的”,那么意味着客户端需要连接。
// operate on the channel...
// client requires a connection
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// get client socket channel
SocketChannel client = server.accept();
// Non Blocking I/O
client.configureBlocking(false);
// record it for read/write operations (Here we have used it for read)
client.register(selector, SelectionKey.OP_READ);
continue;
}
- 如果键是“可读的”,那么意味着服务器已准备好从客户端读取数据。
// if readable then the server is ready to read
if (key.isReadable()) {
SocketChannel client = (SocketChannel) key.channel();
// Read byte coming from the client
int BUFFER_SIZE = 1024;
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
try {
client.read(buffer);
}
catch (Exception e) {
// client is no longer active
e.printStackTrace();
continue;
}
- 如果键是“可写的”,那么意味着服务器已准备好向客户端写入数据。
if (key.isWritable()) {
SocketChannel client = (SocketChannel) key.channel();
// write data to client...
}
现在我们将编写一个简单的客户端来连接服务器。
- 首先,客户端必须创建一个套接字通道以连接到服务器。
SocketAddress address = new InetSocketAddress(hostname, portnumber);
SocketChannel client = SocketChannel.open(address);
- 现在,我们不是要求套接字的输入和输出流,而是将数据写入通道本身。但是,正如我们现在所知道的,我们需要在“ByteBuffer”对象中编码数据才能写入通道。因此,让我们创建一个具有74字节容量的ByteBuffer。
ByteBuffer buffer = ByteBuffer.allocate(74);
- 将缓冲区填充为客户端消息并写入通道。
buffer.put(msg.getBytes());
buffer.flip();
client.write(buffer);
你可以在github中找到NIO场景的完整源代码。
因此,这基本上是如何使用Java NIO创建简单的客户端-服务器,但是,你可以使用可靠的、高性能的网络框架,如netty来满足你的应用程序需求,而不是直接使用Java NIO构建你的应用程序。本文仅旨在帮助你了解阻塞和非阻塞IO的基本理论。
源代码可在github中找到。
参考文献
- https://docs.oracle.com/javase/tutorial/networking/sockets/definition.html
- https://docs.oracle.com/javase/tutorial/networking/sockets/index.html
- http://www.baeldung.com/java-nio-selector
- https://www.manning.com/books/netty-in-action
- https://www.safaribooksonline.com/library/view/learning-java-4th/9781449372477/ch13s05.html
- https://examples.javacodegeeks.com/core-java/nio/java-nio-socket-example/
- https://avaldes.com/java-nio-selectors-using-nio-client-server-example/
- http://www.javaworld.com/article/2073344/core-java/use-select-for-high-speed-networking.html
- http://www.baeldung.com/java-nio-selector
- http://underpop.online.fr/j/java/java-network-programming/javanp3-chp-12-sect-5.html
- http://www.onjava.com/pub/a/onjava/2002/09/04/nio.html?page=2
译自:https://medium.com/coderscorner/tale-of-client-server-and-socket-a6ef54a74763




评论(0)