☆* o(≧▽≦)o *☆嗨~我是小奥
个人博客:小奥的博客
CSDN:个人CSDN
Github:传送门
面经分享(牛客主页):传送门
文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️
Java中的Socket
可以分为普通Socket
和NioSocket
两种。
Java中的网络通信是通过Socket
实现的。Socket
分为ServerSocket
和Socket
两大类 。
ServerSocket
用于服务端,可以通过accept
方法监听请求,监听请求后返回Socket
;Socket
用于具体完成数据传输,客户端直接使用Socket
发起请求并传输数据;ServerSocket
的使用可以分为三步:
ServerSocket
。ServerSocket
的构造方法一共有5个,用起来最方便的是ServerSocket(int port)
,只需要一个port(端口号)即可。ServerSocket
的accept
方法进行监听。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
创建Reader
和Writer
来接收和发送数据,Reader
接收到数据后保存到line
,然后打印到控制台,再将数据发送到client
。
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发送给服务端,然后再接收服务端返回的数据并打印到控制台,最后释放资源关闭连接。
先启动Server
然后启动Clinet
就可以完成一次通信。
Server
运行结果:
Client
运行结果:
从JDK1.4开始,Java增加了新的IO模式 —— nio(new IO),nio在底层采用了新的处理方式,极大地提高了IO的效率。
我们使用的Socket也是IO的一种,nio也提供了相应的工具:ServerSocketChannel
和SocketChannel
,它们分别对应原来的ServerSocket
和Socket
。
想要理解NioSocket必须先理解三个概念:Buffer
、Channel
、Selector
。
我们可以先举一个例子:
之前的送货上门的服务,过程是有客户打电话预约服务,然后服务人员就去处理,提供上门服务,然后完成服务后就继续等待电话,等待下一次服务。(我们假设只有一个服务人员)
这种模式其实就相当于普通Socket
的处理请求的模式,是阻塞式的,每次只能处理一个请求。
但是当有很多请求时,这种模式的弊端就很明显了。
现在的电商网站配送都是以快递的形式,快递会有很多件汇总在一起,进行出库、分拣,并且还要经历中转站,中转站会有分拣员将同一区域的快件给区分开,最后到达每一个快递点。
这样的方式效率就很高了,这种模式就相当于NioSocket
的处理模式,Buffer
就是要送快件,Channel
就是快递送货员,Selector
就是中转站的分拣员。
下面我们来介绍一下它们的概念。
channel
有一点类似于 stream,它就是读写数据的双向通道,可以从 channel
将数据读入 buffer
,也可以将 buffer
的数据写入 channel
,而之前的 stream
要么是输入,要么是输出,channel
比 stream
更为底层。
常见的 Channel 有
Buffer则用来缓冲读写数据,Buffer里面有四个属性非常重要。
capacity
:容量,也就是Buffer最多可以保存元素的数量,在创建时设置,使用过程中不可以改变。limit
:可以使用的上限,开始创建时limit
和capacity
的值相同,如果给limit设置一个值之后,limit
就变成了最大可以访问的值,其值不可以超过capacity
。position
:当前所操作元素所在的索引位置,position
从0开始,随着get
和put
方法自动更新;mark
:用来暂时保存position
的值,position
保存到mark
后就可以修改并进行相关的操作,操作完后可以通过reset
方法将mark
的值恢复到position
。这四个属性的大小关系是:mark <= position <= limit <= capacity
。
常见的 buffer 有
selector 单从字面意思不好理解,需要结合服务器的设计演化来理解它的用途。
多线程版设计
多线程版设计缺点:
线程池版设计
线程池版设计缺点:
selector 版设计
selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景(low traffic)
调用 selector 的 select() 会阻塞直到 channel 发生了读写就绪事件,这些事件发生,select 方法就会返回这些事件交给 thread 来处理。
介绍完这三大组件,我们再来学习如何使用NioSocket
。
NioSocket
的使用可以分为五步:
ServerSocketChannel
并设置相应参数;Selector
并注册到ServerSocketChannel
上;Selector
的select
方法等待请求;Selector
接收到请求后使用selectedKeys
返回selectionKey
集合;SelectionKey
获取到Channel
、Selector
和操作类型并进行具体操作;创建ServerSocketChannel
ServerSocketChannel
可以使用自己的静态工厂方法open
获取。
每个ServerSocketChannel
对应一个ServerSocket
,可以调用其socket
方法获取,但是要注意,需要通过configureBlocking()
方法来设置是否采用阻塞模式,设置了非阻塞模式之后才能调用register
方法注册Selector
使用。(另外,阻塞模式不能使用Selector
)
创建Selector
Selector
可以通过其静态工厂方法open
创建,创建后通过Channel
的register
注册到ServerSocketChannel
或者SocketChannel
上,注册完成之后可以通过select方法来等待请求。
select
方法有一个long类型参数,代表最长等待时间。如果在这段时间内接收到相应操作的请求则可以返回处理的请求的数量,否则超时后返回0.
SelectionKey
SelectionKey保存了处理当前请求的Channel和Selector,并且提供了不同的操作类型。
SelectionKey.OP_ACCEPT
接收请求操作SelectionKey.OP_CONNECT
连接操作SelectionKey.OP_READ
读操作SelectionKey.OP_WRITE
写操作只有在register
方法中注册了相应的操作Selector
才会关心相应类型操作的请求。
现在我们将普通Socket
的Server
改写成使用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();
}
}
}
}