Java中的Socket你了解吗?

Java中的Socket你了解吗?

Java中的Socket可以分为普通SocketNioSocket两种。

1. 普通Socket

Java中的网络通信是通过Socket实现的。Socket分为ServerSocketSocket两大类 。

  • ServerSocket用于服务端,可以通过accept方法监听请求,监听请求后返回Socket
  • Socket用于具体完成数据传输,客户端直接使用Socket发起请求并传输数据;

(1) Server

ServerSocket的使用可以分为三步:

  • **创建ServerSocket**。ServerSocket的构造方法一共有5个,用起来最方便的是ServerSocket(int port),只需要一个port(端口号)即可。
  • 调用创建的ServerSocketaccept方法进行监听accept方法是阻塞方法,也就是调用该方法后程序会停下来等待连接请求,不会继续执行,当接收到请求后accept方法会返回一个Socket
  • 使用accept方法返回的Socket与客户端进行通信。

一个ServerSocket简单使用示例

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

/**
* @Author zal
* @Date 2024/01/15 20:12
* @Description: ServerSocket
* @Version: 1.0
*/
public class Server {
public static void main(String[] args) {
try {
// 创建一个ServerSocket监听8080端口
ServerSocket server = new ServerSocket(8080);
// 等待请求
Socket socket = server.accept();
// 接收到请求后使用socket进行通信,创建BufferedReader用于读取数据
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = is.readLine();
System.out.println("received from client: " + line);
// 创建PrintWriter,用于发送数据
PrintWriter pw = new PrintWriter(socket.getOutputStream());
pw.println("received data:" + line);
pw.flush();
// 关闭资源
pw.close();
socket.close();
server.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

在上述的代码实现中,先创建了ServerSocket,然后调用accept方法等待请求,当接收到请求后,用返回的Socket创建ReaderWriter来接收和发送数据,Reader接收到数据后保存到line,然后打印到控制台,再将数据发送到client

(2) Client

Socket的使用也是一样的:

  • 创建Socket。使用Socket(String host, int port),把目标主机地址和端口号传给Socket;
  • Socket创建的过程就会跟服务端建立连接,然后进行通信即可。

一个Socket的简单使用示例

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

/**
* @Author zal
* @Date 2024/01/15 20:27
* @Description: Client
* @Version: 1.0
*/
public class Client {
public static void main(String[] args) {
String msg = "Client Data";
try {
// 创建一个Socket。跟本机的8080端口连接
Socket socket = new Socket("127.0.0.1", 8080);
// 使用Socket创建的PrintWriter和BufferedReader进行读写数据
PrintWriter pw = new PrintWriter(socket.getOutputStream());
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 发送数据
pw.println(msg);
pw.flush();
// 接收数据
String line = is.readLine();
System.out.println("received from server:" + line);
// 关闭资源
pw.close();
is.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

在上述的代码实现中,创建Socket将msg发送给服务端,然后再接收服务端返回的数据并打印到控制台,最后释放资源关闭连接。

(3) 结果演示

先启动Server然后启动Clinet就可以完成一次通信。

Server运行结果:

image-20240115203438834

Client运行结果:

image-20240115203459976

2. NioSocket

从JDK1.4开始,Java增加了新的IO模式 —— nio(new IO),nio在底层采用了新的处理方式,极大地提高了IO的效率。

我们使用的Socket也是IO的一种,nio也提供了相应的工具:ServerSocketChannelSocketChannel,它们分别对应原来的ServerSocketSocket

想要理解NioSocket必须先理解三个概念:BufferChannelSelector

我们可以先举一个例子:

之前的送货上门的服务,过程是有客户打电话预约服务,然后服务人员就去处理,提供上门服务,然后完成服务后就继续等待电话,等待下一次服务。(我们假设只有一个服务人员)

这种模式其实就相当于普通Socket的处理请求的模式,是阻塞式的,每次只能处理一个请求。

但是当有很多请求时,这种模式的弊端就很明显了。

现在的电商网站配送都是以快递的形式,快递会有很多件汇总在一起,进行出库、分拣,并且还要经历中转站,中转站会有分拣员将同一区域的快件给区分开,最后到达每一个快递点。

这样的方式效率就很高了,这种模式就相当于NioSocket的处理模式,**Buffer就是要送快件,Channel就是快递送货员,Selector就是中转站的分拣员。**

下面我们来介绍一下它们的概念。

(1) Channel

channel 有一点类似于 stream,它就是读写数据的双向通道,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel,而之前的 stream 要么是输入,要么是输出,channel stream 更为底层。

graph LR
channel --> buffer
buffer --> channel

常见的 Channel 有

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

(2) Buffer

Buffer则用来缓冲读写数据,Buffer里面有四个属性非常重要。

  • capacity容量,也就是Buffer最多可以保存元素的数量,在创建时设置,使用过程中不可以改变。
  • limit可以使用的上限,开始创建时limitcapacity的值相同,如果给limit设置一个值之后,limit就变成了最大可以访问的值,其值不可以超过capacity
  • position当前所操作元素所在的索引位置position从0开始,随着getput方法自动更新;
  • mark用来暂时保存position的值position保存到mark后就可以修改并进行相关的操作,操作完后可以通过reset方法将mark的值恢复到position

这四个属性的大小关系是:mark <= position <= limit <= capacity

常见的 buffer 有

  • ByteBuffer
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer

(3) Selector

selector 单从字面意思不好理解,需要结合服务器的设计演化来理解它的用途。

多线程版设计

graph TD
subgraph 多线程版
t1(thread) --> s1(socket1)
t2(thread) --> s2(socket2)
t3(thread) --> s3(socket3)
end

多线程版设计缺点:

  • 内存占用高
  • 线程上下文切换成本高
  • 只适合连接数少的场景

线程池版设计

graph TD
subgraph 线程池版
t4(thread) --> s4(socket1)
t5(thread) --> s5(socket2)
t4(thread) -.-> s6(socket3)
t5(thread) -.-> s7(socket4)
end

线程池版设计缺点:

  • 阻塞模式下,线程仅能处理一个 socket 连接
  • 仅适合短连接场景

selector 版设计

selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景(low traffic)

graph TD
subgraph selector 版
thread --> selector
selector --> c1(channel)
selector --> c2(channel)
selector --> c3(channel)
end

调用 selector 的 select() 会阻塞直到 channel 发生了读写就绪事件,这些事件发生,select 方法就会返回这些事件交给 thread 来处理。

(4) NioSocket

介绍完这三大组件,我们再来学习如何使用NioSocket

NioSocket的使用可以分为五步:

  • 创建ServerSocketChannel并设置相应参数
  • 创建Selector并注册到ServerSocketChannel
  • 调用Selectorselect方法等待请求
  • Selector接收到请求后使用selectedKeys返回selectionKey集合
  • 使用SelectionKey获取到ChannelSelector和操作类型并进行具体操作

创建ServerSocketChannel

ServerSocketChannel可以使用自己的静态工厂方法open获取。

每个ServerSocketChannel对应一个ServerSocket,可以调用其socket方法获取,但是要注意,需要通过configureBlocking()方法来设置是否采用阻塞模式,设置了非阻塞模式之后才能调用register方法注册Selector使用。(另外,阻塞模式不能使用Selector

创建Selector

Selector可以通过其静态工厂方法open创建,创建后通过Channelregister注册到ServerSocketChannel或者SocketChannel上,注册完成之后可以通过select方法来等待请求。

select方法有一个long类型参数,代表最长等待时间。如果在这段时间内接收到相应操作的请求则可以返回处理的请求的数量,否则超时后返回0.

SelectionKey

SelectionKey保存了处理当前请求的Channel和Selector,并且提供了不同的操作类型。

  • SelectionKey.OP_ACCEPT 接收请求操作
  • SelectionKey.OP_CONNECT 连接操作
  • SelectionKey.OP_READ 读操作
  • SelectionKey.OP_WRITE 写操作

只有在register方法中注册了相应的操作Selector才会关心相应类型操作的请求。

现在我们将普通SocketServer改写成使用Nio方式进行处理的NioServer,代码如下:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;

/**
* @Author zal
* @Date 2024/01/15 21:10
* @Description: NioServer
* @Version: 1.0
*/
public class NioServer {
public static void main(String[] args) throws Exception {
// 创建ServerSocketChannel,并监听8080端口
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(8080));
// 设置为非阻塞模式
ssc.configureBlocking(false);
// 为ssc注册选择器
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 创建处理器
Handler handler = new Handler(1024);
while (true) {
// 等待请求,每次等待阻塞3s,超过3s后线程继续向下执行,如果传入0或者不传参数则一直阻塞
if (selector.select(3000) == 0) {
System.out.println("等待请求超时。。。。");
continue;
}
System.out.println("处理请求。。。。");
// 获取等待处理的SelectionKey
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
try {
// 接收连接请求
if (key.isAcceptable()) {
handler.handleAccept(key);
}
// 读数据
if (key.isReadable()) {
handler.handleRead(key);
}
} catch (IOException ex) {
keyIterator.remove();
continue;
}
// 处理完成后,从待处理的SelectionKey中移除当前使用的key
keyIterator.remove();
}
}
}

/**
* 静态内部类,用于处理连接和读取数据
*/
private static class Handler {
private int bufferSize = 1024;
private String localCharset = "UTF-8";

public Handler() {
}

public Handler(int bufferSize) {
this(bufferSize, null);
}

public Handler(String localCharset) {
this(-1, localCharset);
}

public Handler(int bufferSize, String localCharset) {
// 如果指定了有效的缓冲区大小,则使用指定值
if (bufferSize > 0) {
this.bufferSize = bufferSize;
}
// 如果指定了有效的字符集,则使用指定值
if (localCharset != null) {
this.localCharset = localCharset;
}
}

/**
* 处理接受连接事件
* @param key
* @throws IOException
*/
public void handleAccept(SelectionKey key) throws IOException {
// 通过服务器套接字通道接受客户端连接
SocketChannel sc = ((ServerSocketChannel) key.channel()).accept();
// 配置为非阻塞模式
sc.configureBlocking(false);
// 将客户端套接字通道注册到选择器,关注事件为可读,同时附带一个缓冲区
sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}

/**
* 处理读取数据事件
* @param key
* @throws IOException
*/
public void handleRead(SelectionKey key) throws IOException {
// 获取Channel
SocketChannel sc = (SocketChannel) key.channel();
// 获取附加到事件的缓冲区
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear(); // 清空缓冲区,准备读取数据
// 从客户端通道读取数据到缓冲区,如果返回-1表示客户端关闭连接
if (sc.read(buffer) == -1) {
// 关闭channel
sc.close();
} else {
// 切换buffer为读模式
buffer.flip();
// 将buffer中的数据解码为字符串后保存到receivedString
String receivedString = Charset.forName(localCharset).newDecoder().decode(buffer).toString();
System.out.println("received from client: " + receivedString);

// 返回数据给客户端
String sendString = "received data: " + receivedString;
buffer = ByteBuffer.wrap(sendString.getBytes(localCharset));
sc.write(buffer);
sc.close();
}
}
}
}