在本次项目中,我们将实现一个简单的客户端-服务器(Client-Server)通信模型。通过这个项目,你将学习到如何使用Java的SocketCh和ServerSocket类来创建网络连接,进行数据的发送和接收。该项目不仅涵盖了Socket编程的基础知识,还将帮助你理解网络通信中的重要概念,如TCP/IP协议、阻塞与非阻塞I/O等。本socket教程基于NIO的SocketChannal等完成
同时我们会将他部署到自己的云服务器中实现远程消息收发
传统套接字:基于流(Stream),默认为阻塞I/O,在传统的Java Socket编程中,通信是基于流(Stream)的模型。流是一种单向的数据传输机制,分为输入流(InputStream)和输出流(OutputStream)。
缓冲区(Buffer):
NIO的数据读写是基于缓冲区(Buffer)的。缓冲区是一个固定大小的内存块,用于临时存储数据。
缓冲区提供了对数据的结构化访问,例如可以通过position、limit和capacity等属性来管理数据的读写位置。
常见的缓冲区类型包括ByteBuffer、CharBuffer、IntBuffer等
代码流程
服务器
- 首先是创建selctor(NIO核心组件),
- 创建serverSocketChannal
这个serverSocketChannal就理解为以前的ServerSocket就行了,都是监听端口是否有信息的
之前的ServerSocket是只要有一个消息,我们就会返回一个socket - 绑定serverSocketChannal到selctor
- 然后就死循环等待消息到来(有两种连接消息/读消息)
这里会/获取发生事件的 SelectionKey 集合(我们知道NIO多路复用底层用的是epoll,也就是不会遍历所有监听的客户端,而是直接获取)
然后再对遍历集合中的key我们就可以从里面后获取Socketchannel
然后对Socketchannel 就和socket一样,可以从里面获取消息以及发送消息
客户端
- 创建一个socketchannel,然后连接服务器
- 向socketchannel中写东西
- 同时可以接受服务器的东西
- 关闭socketchannel
服务器代码
package coml.zy;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class Main {
public static void main(String[] args) throws IOException {
// 创建 Selector
Selector selector = Selector.open();
// 创建 ServerSocketChannel 并绑定端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//此处应该写0.0.0.0而非localhost
serverSocketChannel.bind(new InetSocketAddress("0.0.0.0", 12345));
serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
// 将 ServerSocketChannel 注册到 Selector,监听 ACCEPT 事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器已启动,等待客户端连接...");
while (true) {
// 阻塞等待事件发生
selector.select();
// 获取发生事件的 SelectionKey 集合
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove(); // 处理完后移除
if (key.isAcceptable()) {
// 处理客户端连接
handleAccept(key, selector);
} else if (key.isReadable()) {
// 处理客户端数据读取
handleRead(key);
}
}
}
}
// 处理客户端连接
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = serverChannel.accept(); // 接受客户端连接
clientChannel.configureBlocking(false); // 设置为非阻塞模式
// 将客户端通道注册到 Selector,监听 READ 事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端已连接:" + clientChannel.getRemoteAddress());
}
// 处理客户端数据读取
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = clientChannel.read(buffer); // 读取客户端数据
if (bytesRead == -1) {
// 客户端断开连接
System.out.println("客户端已断开:" + clientChannel.getRemoteAddress());
clientChannel.close(); // 关闭通道
key.cancel(); // 移除相关的 SelectionKey
return;
}
if (bytesRead > 0) {
buffer.flip(); // 切换为读模式
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String message = new String(data);
System.out.println("收到客户端消息:" + message);
// 向客户端发送响应
String response = "服务器已收到你的消息:" + message;
ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
clientChannel.write(responseBuffer);
}
}
}
然后是一个简单的客户端
package coml.zy;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
public class NioClient2 {
//这里可以换成远程服务器的版本,假如是本地就localhost
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 12345;
public static void main(String[] args) {
try {
startClient();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void startClient() throws IOException {
// 创建 SocketChannel 并连接服务器
SocketChannel socketChannel = SocketChannel.open();
//此处应该写0.0.0.0而非localhost
socketChannel.connect(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
System.out.println("已连接到服务器,可以开始发送消息。输入 'exit' 退出。");
Scanner scanner = new Scanner(System.in);
while (true) {
// 读取用户输入
System.out.print("请输入消息:");
String message = scanner.nextLine();
// 如果用户输入 'exit',则退出循环
if ("exit".equalsIgnoreCase(message)) {
break;
}
// 发送消息给服务器
ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
socketChannel.write(buffer);
System.out.println("已发送消息:" + message);
// 接收服务器的响应
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(responseBuffer);
if (bytesRead > 0) {
responseBuffer.flip();
byte[] data = new byte[responseBuffer.remaining()];
responseBuffer.get(data);
String response = new String(data);
System.out.println("收到服务器响应:" + response);
}
}
// 关闭连接
socketChannel.close();
System.out.println("客户端已断开连接。");
}
}
运行方式
在本地
是将这个服务器打包成JAR包,然后双击,就算是运行了
远程部署
用docker部署,打包成jar包之后
Docker flie
# 使用 OpenJDK 的基础镜像
FROM openjdk:11.0-jre-buster
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY socketDemo-1.0-SNAPSHOT.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
先build再这个run
运行效果
该示例代码由deepSeek大模型辅助生成,deepSeek自我感觉确实比其他的国产大模型靠谱一点,另外比gpt要方便(网络/生成快),gemini最快最傻比国产还傻