网络编程里,TCP 和 UDP 是绕不开的两个传输协议。TCP 保证可靠送达,三次握手之后建立连接,再通过流式传输收发数据;UDP 则是无连接的数据报方式,发出去就不管了,适合实时性要求高但对丢包不敏感的场景。
日常开发中接触更多的是 TCP,尤其是基于 Socket 的编程模型。Java 把底层的细节封装得还算干净,ServerSocket 和 Socket 两个类就能搭起最简单的客户端/服务端通信。写起来不复杂,但有几个点踩过坑之后印象会比较深:
server.accept()是阻塞的,没等到客户端连接就不会往下走。BufferedReader.readLine()读不到数据也会卡住,所以协议里最好约定好结束符或固定长度,不然两边都可能死等。- 带缓冲的输出流记得
flush(),否则数据可能一直留在本地缓冲区没发出去。
一个最简的回显(Echo)服务器大概是这样:
// 服务端
ServerSocket server = new ServerSocket(6666);
Socket socket = server.accept();
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String info = br.readLine();
PrintStream ps = new PrintStream(new BufferedOutputStream(socket.getOutputStream()));
ps.println("echo: " + info);
ps.flush();
ps.close();
br.close();
客户端连上后发一句话,再读回服务器拼接的 "echo: ..."。这种做法简单直接,适合用来理解通信流程,但真正工程里很少这样用:单连接、单线程,多个客户端同时过来就处理不了。
处理多个客户端:线程池 + 每连接一线程
要同时应付多个客户端,经典的做法是主线程只负责 accept,每拿到一个新的 Socket 就交给线程池去处理。这里用 Executors.newFixedThreadPool 限制并发线程数,避免资源耗尽。
ServerSocket server = new ServerSocket(6666);
ExecutorService es = Executors.newFixedThreadPool(3);
while (true) {
Socket socket = server.accept();
System.out.println("连接来自:" + socket.getInetAddress().getHostAddress());
es.execute(new ClientHandler(socket));
}
ClientHandler 的 run 方法里就是针对这一个 socket 的读写逻辑:读一行、处理、写回、关闭流。这里没有做复杂的心跳和异常处理,真实环境还得考虑半关闭、断线重连等细节。
客户端之间的消息转发
如果想让客户端 A 给客户端 B 发消息,就需要服务器做中转。做法是让每个连接在服务端有一个对应的处理线程,并为每个用户起一个唯一的名字(比如登录时指定的用户名)。服务端维护一个集合,存着所有在线用户的处理线程引用。
A 发送消息时,先发给服务器,服务器根据目标名字从集合里找到 B 对应的处理线程,再由那个线程将消息写回 B 的 socket。这个过程中,消息得带上源用户、目标用户、消息体和消息类型,所以定义一个可序列化的 Message 类就很方便。
public class Message implements Serializable {
private String from;
private String to;
private String info;
private int type;
// 构造器、getter/setter 略
}
服务器在读取到客户端发来的 Message 后,根据 type 区分登录消息和普通转发消息。登录时,服务端将当前客户端的名称记录下来,并回复欢迎信息。转发时,遍历集合找到目标线程,把消息写过去。
客户端一边在主线程等待用户输入并发送,一边开一条子线程专门读取服务器转发来的消息,这样才不会漏掉别人发来的内容。
用 Vector<ClientThread> 存线程引用,其实多线程下最好用并发安全的集合,比如 CopyOnWriteArrayList,但这里只是 Demo 用途,暂未深究。
UDP:无连接的数据报
UDP 的代码结构很不一样。接收端和发送端各自创建 DatagramSocket,收数据用的是 DatagramPacket 包裹一个字节数组缓冲区,socket.receive(packet) 会阻塞直到收到数据报。发数据则要指定目标的 InetAddress 和端口。
一个很容易忘的细节:UDP 要先启动接收方,否则发送的数据就丢了——毕竟 UDP 不缓存,发了拉倒。所以哪怕你代码里叫 UDPClient 是接收方,也得让它先跑起来。
// 接收端(Client)
DatagramSocket socket = new DatagramSocket(8000);
byte[] bytes = new byte[1024];
DatagramPacket dp = new DatagramPacket(bytes, bytes.length);
socket.receive(dp);
String info = new String(dp.getData(), 0, dp.getLength());
// 发送端(Server)
DatagramSocket ds = new DatagramSocket(9000);
String s = "good good study, 天天 up";
DatagramPacket dp = new DatagramPacket(
s.getBytes(), 0, s.getBytes().length,
InetAddress.getByName("localhost"), 8000
);
ds.send(dp);
数据报最大 64KB,所以适合发送短消息、实时视频音频流这类控制没那么严格的场合。
用 URL 下载文件
Java 里通过 URL 对象可以方便地拿到网络资源流,再用 HttpURLConnection 进行一些配置。下面这段代码直接从一个美团图片地址下载图片到本地,算是最基础的网络资源抓取。
URL url = new URL("https://p1.meituan.net/biztone/1730f0baefe95984e632a52590f0b446393917.jpg");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("/Users/mac/D/hello.png"));
byte[] bytes = new byte[1024];
int len;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes, 0, len);
}
bis.close();
bos.close();
这里有两点注意:路径要写对文件系统里有权限的位置,另外生产环境还得处理响应码和超时。
MINA 框架:让 NIO 写起来更顺手
原生的 NIO 编程复杂度不低,MINA 框架做了一层封装,用起来思路很清晰:创建 Acceptor/Connector,配置过滤器链,再提供一个 Handler 处理事件。
服务端启动代码:
NioSocketAcceptor acceptor = new NioSocketAcceptor();
DefaultIoFilterChainBuilder chain = acceptor.getFilterChain();
chain.addLast("myChain", new ProtocolCodecFilter(new TextLineCodecFactory()));
acceptor.setHandler(new MinaServerHandler());
acceptor.bind(new InetSocketAddress(9999));
TextLineCodecFactory 帮我们做了粘包拆包,省了不少心。如果需要传输自定义对象,换一个 new ProtocolCodecFilter(new ObjectSerializationCodecFactory()) 就好。
Handler 继承 IoHandlerAdapter,复写几个关键方法:
public class MinaServerHandler extends IoHandlerAdapter {
public void sessionOpened(IoSession session) {
System.out.println("新连接:" + session.getRemoteAddress());
}
public void messageReceived(IoSession session, Object message) {
String msg = (String) message;
session.write("echo:" + msg);
}
public void sessionClosed(IoSession session) {
System.out.println("连接断开");
}
}
客户端也类似,创建 NioSocketConnector 并连接,connectFuture.awaitUninterruptibly() 确保连上再继续。主线程循环读用户输入直接 write 给服务端,收到消息的回显则会在自定义 Handler 里打印。
整个 MINA 部分代码量不多,但已经能跑起来。想深入的话,可以继续研究 IoBuffer、编解码器链、以及如何处理大量长连接。
这些 Demo 覆盖了 Java 网络编程里最常用的几个模式:阻塞 Socket、多线程处理、中转转发、UDP 通信、URL 资源读取、以及基于 MINA 的 NIO 通讯。它们都不完美,但作为理解底层原理和快速上手的基础足够了。实际项目里还得根据需求加上心跳、连接池、超时控制和健壮的异常处理。

