Java 网络编程:Socket 与网络通信
1 引言
在古代,由于通信不便利,人们利用鸽子的飞行能力和方向辨识能力,驯化鸽子进行消息传递,即所谓的“飞鸽传书”。在现代计算机网络中,套接字(Socket)扮演了类似的角色。套接字是应用程序通过网络发送或接收数据的抽象层,允许应用程序将输入输出(I/O)操作应用于网络中,并与其他应用程序进行通信。
2 套接字(Socket)简介
套接字是 IP 地址与端口的组合,是网络通信的基础。通过套接字,应用程序可以像操作文件一样打开、读写和关闭网络连接。
3 网络调试工具:ping 与 telnet
在调试网络程序时,ping
和 telnet
是两个非常有用的工具。
-
ping:用于测试数据包能否通过 IP 协议到达特定主机。
ping
会向目标主机发出一个 ICMP 请求回显数据包,并等待接收回显响应数据包。通过ping
命令,可以检查网络连接是否正常。例如,我们 ping 一下百度。截图如下。
-
telnet:用于远程登录到另一台计算机。通过
telnet
,用户可以在本地计算机上登录到远程计算机,进行交互式操作。在 Windows 系统中,telnet
通常是默认安装但未激活的,可以通过控制面板启用。使用telnet
时,远程计算机需要运行一个服务,该服务持续监听网络连接请求。当接收到客户端的连接请求时,服务器进程会被唤醒,并为两者建立连接,直到某一方中止连接。 -
然而,由于
telnet
是明文传输协议,用户的所有内容(包括用户名和密码)都没有经过加密,因此在现代网络技术中,telnet 的安全性受到质疑,并不被广泛使用。
例如,我们 telnet 一下火(shui)土(mu)社区。截图如下。
4 Socket 实例:客户端
以下是一个简单的 Java 客户端套接字(Socket)示例,模拟 telnet
命令连接远程服务器并读取数据。
import java.io.InputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Scanner;
import java.io.IOException;public class SocketClientExample {public static void main(String[] args) {try (Socket socket = new Socket("bbs.newsmth.net", 23)) {InputStream is = socket.getInputStream();Scanner scanner = new Scanner(is, "gbk");while (scanner.hasNextLine()) {String line = scanner.nextLine();System.out.println(line);}} catch (UnknownHostException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}
}
4.1 代码解析:
-
建立套接字连接:
Socket socket = new Socket("bbs.newsmth.net", 23);
host
为主机名,port
为端口号(23 为默认的 telnet 端口号)。- 如果无法确定主机的 IP 地址,则抛出
UnknownHostException
异常;如果在创建套接字时发生 IO 错误,则抛出IOException
异常。 - 需要注意的是,套接字在建立的时候,如果远程主机不可访问,这段代码会阻塞很长时间,直到底层操作系统的限制而抛出异常。因此,通常会在套接字建立后设置一个超时时间:
socket.setSoTimeout(10000); // 单位为毫秒
-
获取输入流并读取数据:
InputStream is = socket.getInputStream(); Scanner scanner = new Scanner(is, "gbk");while (scanner.hasNextLine()) {String line = scanner.nextLine();System.out.println(line); }
-
通过
java.net.Socket
类的getInputStream()
方法获取输入流。 -
使用
Scanner
类将输入流中的内容按行读取并打印出来。 -
部分结果如下图所示(完整结果建议自己亲手实践一下):
-
通过这种方式,我们可以模拟 telnet
命令的行为,直接与远程主机进行交互,体验网络通信的底层细节。
5 ServerSocket 实例:服务器端
以下是一个简单的 Java 服务器端套接字(ServerSocket)示例,模拟一个远程服务。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;public class ServerSocketExample {public static void main(String[] args) {try (ServerSocket server = new ServerSocket(8888);Socket socket = server.accept();InputStream is = socket.getInputStream();OutputStream os = socket.getOutputStream();Scanner scanner = new Scanner(is)) {PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "gbk"), true);pw.println("你好啊,欢迎关注「沉默王二」公众号,回复关键字「2048」领取程序员进阶必读资料包");boolean done = false;while (!done && scanner.hasNextLine()) {String line = scanner.nextLine();System.out.println(line);if ("2048".equals(line)) {done = true;}}} catch (IOException e) {e.printStackTrace();}}
}
5.1 代码解析:
-
建立服务器端的套接字:
ServerSocket server = new ServerSocket(8888);
- 创建一个
ServerSocket
对象,并指定端口号8888
。端口号0~1023
通常被系统预留,因此我们选择了一个大于1023
的端口号。
- 创建一个
-
等待客户端套接字的连接请求:
Socket socket = server.accept(); InputStream is = socket.getInputStream(); OutputStream os = socket.getOutputStream();
- 调用
ServerSocket
对象的accept()
方法,等待客户端的连接请求。一旦有客户端连接,accept()
方法会返回一个Socket
对象,表示连接已建立。 - 通过
Socket
对象获取输入流和输出流,分别用于读取客户端发送的数据和向客户端发送数据。
- 调用
-
向客户端发送消息:
PrintWriter pw = new PrintWriter(new OutputStreamWriter(os, "gbk"), true); pw.println("你好啊,欢迎关注「沉默王二」 公众号,回复关键字「2048」 领取程序员进阶必读资料包");
- 使用
PrintWriter
将消息发送到客户端。PrintWriter
的构造函数中使用了OutputStreamWriter
来指定字符编码为gbk
,并且第二个参数true
表示自动刷新缓冲区。
- 使用
-
读取客户端发送的消息:
Scanner scanner = new Scanner(is); boolean done = false; while (!done && scanner.hasNextLine()) {String line = scanner.nextLine();System.out.println(line);if ("2048".equals(line)) {done = true;} }
- 使用
Scanner
读取客户端发送的每一行数据。 - 当客户端发送字符串
"2048"
时,服务器端会中断连接,客户端会显示“遗失对主机的连接”。
- 使用
5.2 运行服务并测试:
-
运行服务器端代码:
- 在命令行或 IDE 中运行上述
ServerSocketExample
类。
- 在命令行或 IDE 中运行上述
-
使用
telnet
连接服务器:- 打开一个新的命令行窗口,输入以下命令连接到服务器:
telnet localhost 8888
- 连接成功后,你会看到服务器发送的消息:
你好啊,欢迎关注「沉默王二」 公众号,回复关键字「2048」 领取程序员进阶必读资料包
- 在
telnet
窗口中输入2048
,服务器端会中断连接,telnet
窗口会显示“遗失对主机的连接”。
- 打开一个新的命令行窗口,输入以下命令连接到服务器:
通过这种方式,我们可以模拟一个简单的远程服务,并通过 telnet
进行测试和交互。
6 为多个客户端服务
。以下是优化后的代码示例:
服务器端代码(MultiThreadedServer.java)
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;public class MultiThreadedServer {public static void main(String[] args) throws IOException {int port = 12345;ServerSocket serverSocket = new ServerSocket(port);System.out.println("Server is listening on port " + port);while (true) {Socket socket = serverSocket.accept();System.out.println("Client connected");new ClientHandler(socket).start();}}
}class ClientHandler extends Thread {private Socket socket;public ClientHandler(Socket socket) {this.socket = socket;}public void run() {try {InputStream input = socket.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(input));OutputStream output = socket.getOutputStream();PrintWriter writer = new PrintWriter(output, true);String line;while ((line = reader.readLine()) != null) {System.out.println("Received: " + line);writer.println("Server: " + line);}socket.close();} catch (IOException e) {System.out.println("Client disconnected");}}
}
客户端代码(Client.java)
import java.io.*;
import java.net.Socket;public class Client {public static void main(String[] args) throws IOException {String hostname = "localhost";int port = 12345;Socket socket = new Socket(hostname, port);System.out.println("Connected to the server");InputStream input = socket.getInputStream();BufferedReader reader = new BufferedReader(new InputStreamReader(input));OutputStream output = socket.getOutputStream();PrintWriter writer = new PrintWriter(output, true);writer.println("Hello, server!");String response = reader.readLine();System.out.println("Server response: " + response);socket.close();}
}
6.1 代码解析:
-
服务器端代码:
MultiThreadedServer
类创建一个ServerSocket
并监听指定端口(12345)。- 使用
while (true)
循环不断接受客户端的连接请求。 - 每当有新的客户端连接时,创建一个新的
ClientHandler
线程来处理该连接。
-
ClientHandler 类:
ClientHandler
类继承自Thread
类,用于处理单个客户端的连接。- 在
run()
方法中,处理客户端的输入输出流,并向客户端发送消息。 - 当客户端断开连接时,捕获
IOException
并打印“Client disconnected”。
-
客户端代码:
Client
类创建一个Socket
并连接到服务器。- 通过输入输出流与服务器进行通信。
- 向服务器发送消息并接收服务器的响应。
6.2 运行服务并测试:
-
运行服务器端代码:
- 在命令行或 IDE 中运行
MultiThreadedServer
类。
- 在命令行或 IDE 中运行
-
运行多个客户端:
- 在多个命令行窗口中分别运行
Client
类。 - 每个客户端都会连接到服务器,并向服务器发送消息。
- 服务器会接收客户端的消息,并返回响应。
- 在多个命令行窗口中分别运行
通过这种方式,服务器端可以同时为多个客户端提供服务,每个客户端连接由一个独立的线程处理。这使得服务器能够高效地处理并发连接,满足一对多的需求。
7 UDP 通信:DatagramSocket 实例
UDP 是一种无连接的传输协议,适用于对可靠性要求不高但对速度要求较高的场景。以下是一个简单的 UDP 通信示例。
服务器端代码(UDPServer.java)
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;public class UDPServer {public static void main(String[] args) throws IOException {int port = 12345;DatagramSocket serverSocket = new DatagramSocket(port);System.out.println("Server is listening on port " + port);byte[] buffer = new byte[1024];DatagramPacket packet = new DatagramPacket(buffer, buffer.length);serverSocket.receive(packet);String message = new String(packet.getData(), 0, packet.getLength());System.out.println("Received: " + message);serverSocket.close();}
}
客户端代码(UDPClient.java)
import java.io.IOException;
import java.net.*;public class UDPClient {public static void main(String[] args) throws IOException {String hostname = "localhost";int port = 12345;InetAddress address = InetAddress.getByName(hostname);DatagramSocket clientSocket = new DatagramSocket();String message = "Hello, server!";byte[] buffer = message.getBytes();DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, port);clientSocket.send(packet);System.out.println("Message sent");clientSocket.close();}
}
7.1 代码解析:
-
服务器端代码:
UDPServer
类创建一个DatagramSocket
并监听指定端口(12345)。- 创建一个
DatagramPacket
对象,用于存储接收到的数据包。 - 使用
serverSocket.receive(packet)
方法阻塞,直到收到一个数据包。 - 收到数据包后,从数据包中提取消息并打印。
- 最后关闭
DatagramSocket
。
-
客户端代码:
UDPClient
类解析服务器的 IP 地址。- 创建一个
DatagramSocket
对象。 - 将要发送的消息转换为字节数组,并创建一个
DatagramPacket
对象,指定目标地址和端口。 - 使用
clientSocket.send(packet)
方法发送数据包。 - 最后关闭
DatagramSocket
。
7.2 运行结果:
-
运行服务器端代码:
- 在命令行或 IDE 中运行
UDPServer
类。 - 服务器启动并监听端口
12345
,输出:Server is listening on port 12345
- 在命令行或 IDE 中运行
-
运行客户端代码:
- 在命令行或 IDE 中运行
UDPClient
类。 - 客户端发送消息到服务器,输出:
Message sent
- 在命令行或 IDE 中运行
-
服务器端接收消息:
- 服务器接收到客户端发送的消息,输出:
Received: Hello, server!
- 服务器接收到客户端发送的消息,输出:
通过这个示例,我们可以看到如何使用 DatagramSocket
类实现基于 UDP 协议的通信。UDP 是一种无连接的协议,因此不需要建立连接,发送和接收数据包的速度通常比 TCP 更快,但可靠性较低。
8 总结
掌握 Java Socket 编程对于理解网络通信机制至关重要。通过编写简单的客户端和服务器端程序,可以更好地理解网络通信的基本原理和实现方式。无论是基于 TCP 的 Socket 和 ServerSocket,还是基于 UDP 的 DatagramSocket,都是网络编程中不可或缺的工具。通过实践,可以进一步提升网络编程技能,为开发更复杂的网络应用打下坚实基础。
9 思维导图
10 参考链接
Java Socket:飞鸽传书的网络套接字