基于 socket 手写一个 TCP 服务端及客户端
通过 socket 实现一个 TCP 服务端与客户端,实现通过 TCP 协议进行消息收发。
关键在 socket 的使用的理解上。
socket 是对操作系统提供的协议栈的封装,底层调用的是操作系统提供的协议栈。
当我们调用 ServerSocket 的 accept 方法时,线程阻塞。以 TCP 协议为例,直到网卡接收到一个三次握手的连接请求,网卡向 CPU 发送中断信号,CPU 调用中断处理程序唤醒我们阻塞在 accept 方法上的线程,进行连接处理。
三次握手的过程是由协议栈完成的,我们在应用层编程无法感知。直到三次握手完成,协议栈将客户端信息与服务端信息封装在一个 Socket 对象中返回,我们通过该Socket 对象完成数据的收发。
值得注意的是,在连接建立完成前,操作系统会为本次连接在内核空间开辟两个数据缓冲区:发送缓冲区与接收缓冲区。
我们要做的是监听发送缓冲区是否有数据到达,以及将需要发送的数据写入发送缓冲区。
至于网卡接收到的数据何时由操作系统拆包并写入接收缓冲区,以及我们写入发送缓冲区的数据何时会被封装为 TCP 报文发送给网卡是操作系统 OS 控制的,这对我们来说是透明的。不同的 OS 对此会有不同的实现,我们不需要关注这些细节(或者说想要关注也没办法介入)。
本次实现是通过传统的 ServerSocket 建立服务端,并没有使用通道技术。也就是说是 BIO 的实现,当并发量比较大时可以采用 NIO 多路复用技术进行优化。这可以帮助我们节约线程数。
对 TCP 报文进行分包有多种方式,本次实现使用的是最普适的方式,通过报文头添加报文长度字段进行分包,也就是与 HTTP 协议 Header 中的 Content-Length 相同的方式。
测试时客户端发送的数据是一个序列化的对象,服务端对其进行反序列化并检查结果。
由于牵扯到线程的切换,本次实现并没有对代码结构进行提前设计,仅仅是简单的实现了数据收发功能。经过设计优化的代码将在下篇博客发出。
服务端:
/** * @Author Nxy * @Date 2020/3/21 17:16 * @Description socket 服务端 */ public class BasicSeverDemo { public static void main(String[] args) { ServerSocket server = null; try { server = new ServerSocket(80); System.out.println("server start!"); } catch (IOException e) { e.printStackTrace(); return; } while (!Thread.currentThread().isInterrupted()) { Socket socket; BufferedInputStream in; BufferedOutputStream out; try { //阻塞等待连接请求 socket = server.accept(); System.out.println("建立连接:" + socket.getInetAddress()); in = new BufferedInputStream(socket.getInputStream()); out = new BufferedOutputStream(socket.getOutputStream()); } catch (IOException e) { e.printStackTrace(); System.out.println("连接建立失败!"); continue; } byte[] result; try { //阻塞等待接收请求数据 byte[] lengthByte = IOUtil.readBytesFromInputStream(in, 4); //本次请求的长度 int length = ByteBuffer.wrap(lengthByte).getInt(); System.out.println("from server:" + length); //读取指定长度字节 result = IOUtil.readBytesFromInputStream(in, length); } catch (Exception e) { e.printStackTrace(); break; } //反序列化对象 Invocation obj = null; try { ByteArrayInputStream bis = new ByteArrayInputStream(result); ObjectInputStream ois = new ObjectInputStream(bis); obj = (Invocation) ois.readObject(); ois.close(); bis.close(); } catch (IOException ex) { ex.printStackTrace(); } catch (ClassNotFoundException ex) { ex.printStackTrace(); } System.out.println(obj.getInterfaceName() + ":" + obj.getMethodName()); } }
客户端:
/** * @Author Nxy * @Date 2020/3/21 17:54 * @Description socket 客户端 */ public class BasicClientDemo { public static void main(String[] args) { Socket socket; BufferedOutputStream out; BufferedInputStream in; try { socket = new Socket("127.0.0.1", 80); out = new BufferedOutputStream(socket.getOutputStream()); in = new BufferedInputStream(socket.getInputStream()); } catch (IOException e) { e.printStackTrace(); return; } Object[] params = new Object[2]; Class[] paramTypes = new Class[2]; Invocation invocation = new Invocation(BasicClientDemo.class.getName(), "main", paramTypes, params); byte[] invocationBytes = toByteArray(invocation); int length = invocationBytes.length; try { System.out.println("from client:" + length); out.write(ByteBuffer.allocate(4).putInt(length).array()); out.flush(); out.write(invocationBytes); out.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } } }
工具类:
public class IOUtil { /** * @Author Nxy * @Date 2020/3/21 20:21 * @Param in:输入流,length:读取字节数 * @Return * @Exception * @Description 从输入流读取指定长度字节的数据 */ public static byte[] readBytesFromInputStream(BufferedInputStream in, int length) throws IOException { int readSize; byte[] bytes = null; bytes = new byte[length]; long length_tmp = length; long index = 0;// start from zero while ((readSize = in.read(bytes, (int) index, (int) length_tmp)) != -1) { length_tmp -= readSize; if (length_tmp == 0) { break; } index = index + readSize; } return bytes; }
public static byte[] toByteArray(Object obj) {
byte[] bytes = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
oos.flush();
bytes = bos.toByteArray();
oos.close();
bos.close();
} catch (IOException ex) {
ex.printStackTrace();
}
return bytes;
}
}
执行效果,服务端正常接收到数据并成功反序列化为对象:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构