如何做网站刷题,网页设计与网站建设第05,网络店铺的营销方法,网络营销岗位招聘信息文章目录 Java网络编程原理与实践--从Socket到BIO再到NIOSocket基本架构Socket 基本使用简单一次发送接收客户端服务端 字节流方式简单发送接收客户端服务端 双向通信客户端服务端 多次接收消息客户端服务端 Socket写法的问题BIO简单流程BIO写法客户端服务端 BIO的问题 NIO简述… 文章目录 Java网络编程原理与实践--从Socket到BIO再到NIOSocket基本架构Socket 基本使用简单一次发送接收客户端服务端 字节流方式简单发送接收客户端服务端 双向通信客户端服务端 多次接收消息客户端服务端 Socket写法的问题BIO简单流程BIO写法客户端服务端 BIO的问题 NIO简述BufferChannel通道Selector选择器基本介绍使用实例 Java网络编程原理与实践–从Socket到BIO再到NIO
Socket基本架构
图来源https://zhuanlan.zhihu.com/p/462497498 既然是网络的东西肯定得放个网络架构图这张图不多说感兴趣可以去链接详细看一下
Socket 基本使用
转自https://blog.csdn.net/a78270528/article/details/80318571
简单一次发送接收
客户端
package Scoket.client;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;/**** 字符流方式*/
public class Client {public static void main(String[] args) {try {// 服务器的主机和端口String serverHost 127.0.0.1;int serverPort 6443;// 创建Socket对象连接到服务器Socket socket new Socket(serverHost, serverPort);// 获取输入流和输出流BufferedReader input new BufferedReader(new InputStreamReader(socket.getInputStream()));PrintWriter output new PrintWriter(socket.getOutputStream(), true);// 发送数据给服务器String messageToSend Hello, Server!;output.println(messageToSend);// 接收服务器的响应数据String dataReceived input.readLine();System.out.println(Received from server: dataReceived);// 关闭连接socket.close();} catch (Exception e) {e.printStackTrace();}}
}
服务端
package Scoket.client;import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;public class Server {public static void main(String[] args) {try {// 本地主机和端口String serverHost 127.0.0.1;int serverPort 6443;// 创建ServerSocket对象绑定地址和端口ServerSocket serverSocket new ServerSocket(serverPort);System.out.println(Server listening on serverHost : serverPort);// 接受客户端连接Socket clientSocket serverSocket.accept();System.out.println(Accepted connection from clientSocket.getInetAddress());// 获取输入流和输出流BufferedReader input new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));PrintWriter output new PrintWriter(clientSocket.getOutputStream(), true);// 接收客户端发送的数据String dataReceived input.readLine();System.out.println(Received from client: dataReceived);// 发送响应给客户端String messageToSend Hello, Client!;output.println(messageToSend);// 关闭连接clientSocket.close();serverSocket.close();} catch (Exception e) {e.printStackTrace();}}
}如果进行debug会发现服务端代码总共卡主两次 1、 Socket clientSocket serverSocket.accept(); 这里会监听端口等待客户端请求建立连接实际上是进行三次握手 2、 String dataReceived input.readLine(); 这里是等待客户端发送数据接收到数据会进行下一步 这两步骤需要注意因为这是后面BIO和NIO的优化点
字节流方式简单发送接收
使用字节流处理这可能使得处理字符串数据稍显繁琐。如果你的通信数据是文本可能使用字符流更为方便。 但是数据更可控一些下面简单罗列
客户端
package Scoket.client;import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;/*** 字节流方式*/
public class Client1 {public static void main(String[] args) {try {String host 127.0.0.1;int port 6443;Socket socket new Socket(host, port);OutputStream outputStream socket.getOutputStream();String message message 你好;socket.getOutputStream().write(message.getBytes(StandardCharsets.UTF_8));outputStream.close();socket.close();} catch (IOException e) {e.printStackTrace();}}
}
服务端
package Scoket.client;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;public class Server1 {public static void main(String[] args) {try {int port 6443;ServerSocket serverSocket new ServerSocket(port);System.out.println(等待连接);Socket accept serverSocket.accept();System.out.println(完成连接等待传输数据);InputStream inputStream accept.getInputStream();byte[] bytes new byte[1024];int len;StringBuilder sb new StringBuilder();while ((len inputStream.read(bytes)) ! -1){sb.append(new String(bytes, 0, len, UTF-8));}System.out.println(get message: sb);inputStream.close();accept.close();serverSocket.close();} catch (IOException e) {e.printStackTrace();}}
}双向通信
客户端
package Scoket.client;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;public class Client2 {public static void main(String[] args) {try {String host 127.0.0.1;int port 8443;Socket socket new Socket(host, port);OutputStream outputStream socket.getOutputStream();outputStream.write(我是客户接受一下我的消息.getBytes(StandardCharsets.UTF_8));socket.shutdownOutput();InputStream inputStream socket.getInputStream();byte[] bytes new byte[1024];int len;StringBuilder stringBuilder new StringBuilder();while ((len inputStream.read(bytes)) ! -1){stringBuilder.append(new String(bytes, 0, len, UTF-8));}System.out.println(get message: stringBuilder);inputStream.close();outputStream.close();socket.close();} catch (IOException e) {e.printStackTrace();}}
}
服务端
package Scoket.client;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;public class Server2 {public static void main(String[] args) {try {int port 8443;ServerSocket serverSocket new ServerSocket(port);Socket socket serverSocket.accept();InputStream inputStream socket.getInputStream();byte[] bytes new byte[1024];int len ;StringBuilder sb new StringBuilder();while ((len inputStream.read(bytes)) ! -1){sb.append(new String(bytes, 0, len, UTF-8));}System.out.println(server2 get Message: sb);OutputStream outputStream socket.getOutputStream();outputStream.write(我是服务器.getBytes(StandardCharsets.UTF_8));inputStream.close();outputStream.close();socket.close();serverSocket.close();} catch (IOException e) {e.printStackTrace();}}
}多次接收消息
客户端
package Scoket.client;import java.io.OutputStream;
import java.net.Socket;public class Client3 {public static void main(String args[]) throws Exception {// 要连接的服务端IP地址和端口String host 127.0.0.1;int port 8444;// 与服务端建立连接Socket socket new Socket(host, port);// 建立连接后获得输出流OutputStream outputStream socket.getOutputStream();String message 你好 yiwangzhibujian;//首先需要计算得知消息的长度byte[] sendBytes message.getBytes(UTF-8);//然后将消息的长度优先发送出去outputStream.write(sendBytes.length 8);outputStream.write(sendBytes.length);//然后将消息再次发送出去outputStream.write(sendBytes);outputStream.flush();//此处重复发送一次实际项目中为多个命名此处只为展示用法message 第二条消息;sendBytes message.getBytes(UTF-8);outputStream.write(sendBytes.length 8);outputStream.write(sendBytes.length);outputStream.write(sendBytes);outputStream.flush();//此处重复发送一次实际项目中为多个命名此处只为展示用法message the third message!;sendBytes message.getBytes(UTF-8);outputStream.write(sendBytes.length 8);outputStream.write(sendBytes.length);outputStream.write(sendBytes); outputStream.close();socket.close();}
}服务端
package Scoket.client;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;public class Server3 {public static void main(String[] args) {try {int port 8444;ServerSocket serverSocket new ServerSocket(port);Socket accept serverSocket.accept();InputStream inputStream accept.getInputStream();byte[] bytes;while (true){int first inputStream.read();if(first -1){break;}int second inputStream.read();int len (first 8) second;bytes new byte[len];inputStream.read(bytes);System.out.println(Server3 get message: new String(bytes, UTF-8));}} catch (IOException e) {e.printStackTrace();}}
}
Socket写法的问题
上面的代码有些很大的问题 1、阻塞式 I/O 这是最大的缺点之一。在 accept()、readLine() 等方法调用时程序会被阻塞等待客户端连接或数据到来。这可能导致服务器在处理多个客户端时性能下降。 2、单线程处理 服务器采用单线程处理客户端连接。这意味着一次只能处理一个客户端连接如果有大量的客户端同时连接性能会受到影响。 3、不适用于高并发 由于采用单线程处理方式不适合高并发环境。在高并发情况下建议考虑使用多线程或异步 I/O 模型。 4、异常处理不足 缺少一些异常处理例如在 accept()、readLine() 中可能会抛出异常而在示例中并未捕获和处理这些异常。 针对1、2可以采用BIO方式 针对1、2、3可以采用NIO 接下来将会优化代码分别介绍BIO和NIO
BIO
简单流程
服务器启动一个ServerSocket。 客户端启动一个Socket对服务器进行通信默认情况下服务器端需要对每一个客户端建立一个线程与之通信。 客户端发出请求后先咨询服务器是否有线程相应如果没有则会等待或者被拒绝。 如果有响应客户端线程会等待请求结束后再继续执行。
BIO写法
客户端
package Scoket.client;import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;public class BIOClient{public static void main(String[] args) {try {Socket socket new Socket(127.0.0.1, 6666);OutputStream outputStream socket.getOutputStream();outputStream.write(hi, i am client.getBytes(StandardCharsets.UTF_8));outputStream.flush();socket.close();} catch (IOException e) {e.printStackTrace();}}
}服务端
转自https://juejin.cn/post/6924670437867651080
package Scoket.client;import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BIOServer {public static void main(String[] args) throws IOException {// 创建线程池ExecutorService executorService Executors.newCachedThreadPool();// 创建ServerSocket并且监听6666端口ServerSocket serverSocket new ServerSocket(6666);while (true) {// 监听---一直等待客户端连接Socket socket serverSocket.accept();// 连接来了之后启用一个线程去执行里面的方法executorService.execute(() - {try {// 获取客户端发送过来的输入流InputStream inputStream socket.getInputStream();byte[] bytes new byte[1024];int read inputStream.read(bytes);// 读取发送过来的信息并打印if (read ! -1) {System.out.println(new String(bytes, 0, read));}} catch (IOException e) {e.printStackTrace();} finally {// 断开通讯try {socket.close();} catch (IOException e) {e.printStackTrace();}}});}}
}BIO的问题
上述写法主要是在服务端接受到一个客户端连接时就开启一个线程然后新建一个连接专门处理这个服务 可以看下accept代码 public Socket accept() throws IOException {if (this.isClosed()) {throw new SocketException(Socket is closed);} else if (!this.isBound()) {throw new SocketException(Socket is not bound yet);} else {Socket s new Socket((SocketImpl)null);this.implAccept(s);return s;}}
可以看到每次accept就会新建一个Socket 因此会有如下问题 每个请求都需要创建独立的线程与对应的客户端进行数据读业务处理然后再数据写。 当并发数较大时需要创建大量的线程来处理连接系统资源占用较大。 连接建立后如果当前线程暂时没有数据可读则线程就阻塞在读操作上造成线程资源浪费。
基于上面的问题产生了NIO
NIO
简述
Java NIO全称java non-blocking IO是指JDK提供的新API。从JDK1.4开始提供了一系列改进的输入/输出的新特性被统称为NIO所以也可称为New IO是同步非阻塞的。 NIO相关类都被放在java.nio包及子包下并且对原java.io包中的很多类进行改写。 NIO有三大核心部分 Channel通道 Buffer缓冲区 Selector选择器 NIO是面向缓冲区的。数据读取到一个它的稍后处理的缓冲区需要时可以在缓冲区中前后移动这就增加了处理过程中的灵活性使用它可以提供非阻塞式的高伸缩性网络。 Java NIO的非阻塞模式是一个线程从某通道发送请求或者读取数据但是它仅能得到目前可用的数据如果目前没有数据可用时就什么都不会获取而不是保持线程阻塞所以直至数据变得可以读取之前该线程可以继续做其他的事情。非阻塞写也是如此一个线程请求写入一些数据到某通道但不需要等待它完全写入这个线程同时可以去做别的事情。 通俗理解NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来根据实际情况可以分配50或者100个线程来处理。不像之前的阻塞IO那样非得分配10000个。 HTTP2.0使用了多路复用的技术做到了同一个连接并发处理多个请求而且并发请求的数量比HTTP1.1大了好几个数量级。
Buffer
Buffer缓冲区缓冲区本质上是一个可以读写数据的内存块可以理解成是一个容器对象含数组该对象提供了一组方法可以更轻松的使用内存块缓冲区对象内置了一些机制能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道但是读取或写入的数据都必须经由Buffer。 在使用Buffer进行数据读写的时候主要是通过底层的这个数组来储存数据但是具体的控制数据读写是通过父类Buffer中的以下参数来控制的
属性描述Capacity容量即可以容纳的最大数据量。在缓冲区被创建时被确定并且不能改变Limit示缓冲区的当前终点不能对缓冲区超过limit的位置进行读写操作且limit是可以修改的Position位置下一个要被读/写的元素的索引每次读写缓冲区数据时都会改变position的值为下次读写做准备Mark标记
一共有7个类直接继承了Buffer类这7个子类分别是除了boolean外的其他7中数据类型的Buffer类。 在这七个子类中都有一个相应数据类型的数组比如IntBuffer中就有一个int类型的数组 final int[] hb; 在ByteBuffer类中就有一个byte类型的数组 final byte[] hb; 实例
package Scoket.client;import java.nio.IntBuffer;public class Buffer {public static void main(String[] args) {// 创建一个IntBuffer对象实例分配容量为5IntBuffer buffer IntBuffer.allocate(5);for (int i 0; i buffer.capacity(); i) {// 每次循环为buffer塞一个int类型的数值经过5次循环后buffer中应该有0、2、4、6、8这5个数buffer.put(i * 2);}// 当要将buffer从写入转换到读取的时候需要调用flip()方法// flip()方法是将limit指向position的位置并且再将position置0// 表示从头再读到调用flip()方法的地方buffer.flip();// hasRemaining()方法表示是否还有剩余的元素可读取// 里面是通过position limit判断是否有剩余的元素while (buffer.hasRemaining()) {System.out.println(buffer.get());}// 这时将position的位置设置成1limit的位置设置成4buffer.position(1);buffer.limit(4);// 因为不能读取超过limit的元素并且从position位置开始读取所以这里将会输出2、4、6while (buffer.hasRemaining()) {System.out.println(buffer.get());}}
}
Channel通道
NIO的通道类似于流但两者之间有所区别 通道可以同时进行读写而流只能读或者只能写 通道可以实现异步读写数据 通道可以从缓冲区读取数据也可以写数据到缓冲区 BIO的stream是单向的例如FileInputStream对象只能进行读取数据的操作而NIO中的通道Channel是双向的可以读操作也可以写操作。 Channel在NIO中是一个接口。 常用的Channel类有FileChannel、DatagramChannel、ServerSocketChannel、SocketChannel。FileChannel用于文件的数据读写DatagramChannel用于UDP的数据读写ServerSocketChannel和SocketChannel用于TCP的数据读写。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;public class Channel {public static void main(String[] args) throws Exception {// 从桌面上随机取一张图片进行复制操作// 获取原图片和被复制图片路径的流FileInputStream fileInputStream new FileInputStream(/src/main/resources/img.png);FileOutputStream fileOutputStream new FileOutputStream(/src/main/resources/img_1.png);// 通过流的getChannel()方法获取两个通道FileChannel fileInputStreamChannel fileInputStream.getChannel();FileChannel fileOutputStreamChannel fileOutputStream.getChannel();// 创建一个字节类型的缓冲区并为其分配1024长度ByteBuffer byteBuffer ByteBuffer.allocate(1024);// 每次读取图片的字节到缓冲区当读返回-1时表示读完了while (fileInputStreamChannel.read(byteBuffer) -1) {// 调用flip()方法从读的状态变为写的状态byteBuffer.flip();// 复制将缓冲区中的数据写入到管道中fileOutputStreamChannel.write(byteBuffer);// 将缓冲区清空以便于下一次读取byteBuffer.clear();}// 关闭Closeable对象fileOutputStreamChannel.close();fileInputStreamChannel.close();fileOutputStream.close();fileInputStream.close();}
}
Selector选择器
基本介绍
Java的NIO用非阻塞的IO方式。可以用一个线程处理多个的客户端连接就会使用到Selector选择器。 Selector能够检测多个注册的通道上是否有事件发生如果有事件发生便获取时间然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道也就是管理多个连接和请求。 只有在连接通道真正有读写事件发生时才会进行读写就大大地减少了系统开销并且不必为每一个连接都创建一个线程不用去维护多个线程。避免了多个线程之间的上下文切换导致的开销 SelectionKey为Selector中有一个Channel注册了就会生成一个SelectionKey对象在同步非阻塞中Selector可以通过SelectionKey找到相应的Channel并处理。 SelectionKey在Selector和Channel的注册关系中一共分为四种
Int OP_ACCEPT有新的网络连接可以accept值为1614 int OP_CONNECT代表连接已经建立值为8(13) int OP_WRITE代表写操作值为4(12) int OP_READ代表读操作值为1(10)
使用实例
客户端
package Scoket.client;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;public class NioClient {public static void main(String[] args) throws IOException {// 连接服务器SocketChannel socketChannel SocketChannel.open();socketChannel.connect(new InetSocketAddress(localhost, 6443));// 发送数据String message Hello, Server!;ByteBuffer buffer ByteBuffer.wrap(message.getBytes(UTF-8));socketChannel.write(buffer);System.out.println(Sent to server: message);// 关闭连接socketChannel.close();}
}
服务端
package Scoket.client;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.util.Iterator;
import java.util.Set;public class NioServer {public static void main(String[] args) throws IOException {// 打开 SelectorSelector selector Selector.open();// 打开 ServerSocketChannel监听客户端连接ServerSocketChannel serverSocketChannel ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(6443));// 设置为非阻塞模式serverSocketChannel.configureBlocking(false);// 注册接受连接事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println(Server listening on port 6443);while (true) {// 阻塞直到有就绪事件发生int readyChannels selector.select();if (readyChannels 0) {continue;}// 获取就绪事件的 SelectionKey 集合SetSelectionKey selectedKeys selector.selectedKeys();IteratorSelectionKey keyIterator selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key keyIterator.next();if (key.isAcceptable()) {// 有新的连接handleAccept(key, selector);} else if (key.isReadable()) {// 有数据可读handleRead(key);}keyIterator.remove();}}}private static void handleAccept(SelectionKey key, Selector selector) throws IOException {ServerSocketChannel serverSocketChannel (ServerSocketChannel) key.channel();SocketChannel socketChannel serverSocketChannel.accept();socketChannel.configureBlocking(false);// 注册读事件socketChannel.register(selector, SelectionKey.OP_READ);System.out.println(Accepted connection from: socketChannel.getRemoteAddress());}private static void handleRead(SelectionKey key) throws IOException {SocketChannel socketChannel (SocketChannel) key.channel();ByteBuffer buffer ByteBuffer.allocate(1024);int bytesRead socketChannel.read(buffer);if (bytesRead 0) {buffer.flip();byte[] data new byte[bytesRead];buffer.get(data);System.out.println(Received from client: new String(data, UTF-8));// 在这里可以添加业务逻辑然后将响应数据写入到 SocketChannel// ...// 关闭连接socketChannel.close();}}
}
参考源 https://zhuanlan.zhihu.com/p/462497498 https://blog.csdn.net/a78270528/article/details/80318571 https://juejin.cn/post/6924670437867651080 https://juejin.cn/post/6925046428213608456