Appearance
Java IO - 07 TCP通信
本文介绍java.net下的TCP通信编程,涉及ServerSocket、Socket和线程相关知识。
ServerSocket表示服务端程序,用于监听客户端连接;
Socket表示服务端与客户端之间的通道,可通过Socket对象获取输入与输出流:
getInputStream():获取输入流以读取数据;getOutputStream():获取输出流以写出数据;
1. 案例一:一发一收
该案例实现客户端发送一条消息,服务端接收消息并显示。
服务端代码如下:
java
@Slf4j
public class ServerDemo {
public static void main(String[] args) throws IOException {
// 1. 创建服务端程序,监听8888端口
ServerSocket serverSocket = new ServerSocket(8888);
log.info("服务端启动,监听端口:{}", 8888);
// 2. 阻塞等待客户端连接
Socket socket = serverSocket.accept();
SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress(); // 获取客户端地址
log.info("{} 连接成功", remoteSocketAddress);
// 3. 获取输入流,以读取客户端发送的数据
InputStream inputStream = socket.getInputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream);
// 4. 阻塞等待客户端发送数据
String data = dataInputStream.readUTF();
// 5. 打印客户端发送的数据
log.info("{} 发送:{}",remoteSocketAddress, data);
// 6. 关闭资源
dataInputStream.close();
}
}客户端代码如下:
java
public class ClientDemo {
public static void main(String[] args) throws IOException {
// 1. 与服务端建立连接
Socket socket = new Socket("127.0.0.1", 8888);
// 2. 获取输出流以输出数据
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
// 3. 输出数据
dataOutputStream.writeUTF("你好");
// 4. 关闭资源
dataOutputStream.close();
socket.close();
}
}结果如下:
txt
2025-02-06 16:52:32 [main] INFO com.lee.tcp.ServerDemo - 服务端启动,监听端口:8888
2025-02-06 16:52:37 [main] INFO com.lee.tcp.ServerDemo - /127.0.0.1:65252 连接成功
2025-02-06 16:52:37 [main] INFO com.lee.tcp.ServerDemo - /127.0.0.1:65252 发送:你好2. 案例二:多发多收
该案例实现客户端可以读取用户在控制台的输入,并发送给服务端,服务端显示多条消息。当用户输入exit时,退出程序。
客户端代码:
java
public class ClientDemo {
public static void main(String[] args) throws IOException {
// 1. 与服务端建立连接
Socket socket = new Socket("127.0.0.1", 8888);
// 2. 获取输出流以输出数据
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
// 用户输入消息并发送给服务端
Scanner scanner = new Scanner(System.in);
while (true){
String input = scanner.nextLine();
if("exit".equals(input)) {
// 4. 关闭资源
dataOutputStream.close();
socket.close();
break;
}
// 3. 输出数据
dataOutputStream.writeUTF(input);
}
}
}服务端代码:
java
@Slf4j
public class ServerDemo {
public static void main(String[] args) throws IOException {
// 1. 创建服务端程序,监听8888端口
ServerSocket serverSocket = new ServerSocket(8888);
log.info("服务端启动,监听端口:{}", 8888);
// 2. 阻塞等待客户端连接
Socket socket = serverSocket.accept();
SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress(); // 获取客户端地址
log.info("{} 连接成功", remoteSocketAddress);
// 3. 获取输入流,以读取客户端发送的数据
InputStream inputStream = socket.getInputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream);
try {
// 循环等待
while (true) {
// 4. 阻塞等待客户端发送数据
String data = dataInputStream.readUTF();
// 5. 打印客户端发送的数据
log.info("{} 发送:{}", remoteSocketAddress, data);
}
}catch (Exception e){
log.error("{} 退出了", remoteSocketAddress);
// 6. 关闭资源
dataInputStream.close();
}
}
}效果如下:
java
2025-02-06 17:00:01 [main] INFO com.lee.tcp.ServerDemo - 服务端启动,监听端口:8888
2025-02-06 17:00:04 [main] INFO com.lee.tcp.ServerDemo - /127.0.0.1:65390 连接成功
2025-02-06 17:00:09 [main] INFO com.lee.tcp.ServerDemo - /127.0.0.1:65390 发送:123
2025-02-06 17:00:11 [main] INFO com.lee.tcp.ServerDemo - /127.0.0.1:65390 发送:你好
2025-02-06 17:00:12 [main] INFO com.lee.tcp.ServerDemo - /127.0.0.1:65390 发送:hello
2025-02-06 17:00:17 [main] ERROR com.lee.tcp.ServerDemo - /127.0.0.1:65390 退出了3. 多客户端连接:多线程版
在上面的案例中,服务端只能服务一个客户端,我们可以使用线程,让一个线程服务一个客户端接收消息。

服务端代码:
java
@Slf4j
public class ServerDemo {
public static void main(String[] args) throws IOException {
// 创建服务端程序,监听8888端口
ServerSocket serverSocket = new ServerSocket(8888);
log.info("服务端启动,监听端口:{}", 8888);
// 阻塞等待客户端连接
while (true) {
Socket socket = serverSocket.accept();
// 创建线程,处理客户端请求
new Thread(new ReceiverThread(socket)).start();
}
}
}
@Slf4j
class ReceiverThread implements Runnable{
private Socket socket;
public ReceiverThread(Socket socket){
this.socket = socket;
}
@Override
public void run() {
SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress(); // 获取客户端地址
log.info("{} 连接成功", remoteSocketAddress);
// 获取输入流,以读取客户端发送的数据
InputStream inputStream = null;
DataInputStream dataInputStream = null;
try {
inputStream = socket.getInputStream();
dataInputStream = new DataInputStream(inputStream);
// 循环等待
while (true) {
// 阻塞等待客户端发送数据
String data = dataInputStream.readUTF();
// 打印客户端发送的数据
log.info("{} 发送:{}", remoteSocketAddress, data);
}
}catch (Exception e){
log.error("{} 退出了", remoteSocketAddress);
// 6. 关闭资源
try {
dataInputStream.close();
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
}效果如下:
txt
2025-02-06 17:09:29 [main] INFO com.lee.tcp.ServerDemo - 服务端启动,监听端口:8888
2025-02-06 17:09:35 [Thread-0] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49273 连接成功
2025-02-06 17:09:38 [Thread-1] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49276 连接成功
2025-02-06 17:09:44 [Thread-1] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49276 发送:111
2025-02-06 17:09:47 [Thread-0] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49273 发送:222
2025-02-06 17:09:52 [Thread-1] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49276 发送:你好
2025-02-06 17:09:56 [Thread-0] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49273 发送:hello
2025-02-06 17:09:58 [Thread-0] ERROR com.lee.tcp.ReceiverThread - /127.0.0.1:49273 退出了
2025-02-06 17:10:01 [Thread-1] ERROR com.lee.tcp.ReceiverThread - /127.0.0.1:49276 退出了可以看到同时有两个客户端与服务端通信。
4. 多客户端连接:线程池版
多线程版的缺点在于,如果有非常多客户端与服务端通信,那么服务端会创建很多线程,将会造成内存溢出程序崩溃,所以我们可以使用线程池改进。

服务端代码:
java
@Slf4j
public class ServerDemo {
public static void main(String[] args) throws IOException {
// 创建服务端程序,监听8888端口
ServerSocket serverSocket = new ServerSocket(8888);
log.info("服务端启动,监听端口:{}", 8888);
// 创建线程池
ExecutorService pool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2,
Runtime.getRuntime().availableProcessors() * 2,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
// 阻塞等待客户端连接
while (true) {
Socket socket = serverSocket.accept();
// 向线程池中提交任务
pool.submit(new ReceiverThread(socket));
}
}
}
// ReceiverThread与上面的相同,省略...效果如下:
txt
2025-02-06 17:25:07 [main] INFO com.lee.tcp.ServerDemo - 服务端启动,监听端口:8888
2025-02-06 17:25:11 [pool-1-thread-1] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49488 连接成功
2025-02-06 17:25:17 [pool-1-thread-2] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49492 连接成功
2025-02-06 17:25:20 [pool-1-thread-3] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49495 连接成功
2025-02-06 17:25:22 [pool-1-thread-1] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49488 发送:111
2025-02-06 17:25:25 [pool-1-thread-2] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49492 发送:222
2025-02-06 17:25:28 [pool-1-thread-3] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49495 发送:333
2025-02-06 17:25:33 [pool-1-thread-1] ERROR com.lee.tcp.ReceiverThread - /127.0.0.1:49488 退出了
2025-02-06 17:25:39 [pool-1-thread-3] ERROR com.lee.tcp.ReceiverThread - /127.0.0.1:49495 退出了
2025-02-06 17:25:42 [pool-1-thread-2] ERROR com.lee.tcp.ReceiverThread - /127.0.0.1:49492 退出了使用线程池的问题在于,如果TCP连接是长连接,那么线程池中的线程会被占满,导致后续的客户端连接无法被服务,例如,我们可以将上面的连接池大小改为2,那么第三个客户端连接将无法及时响应:
txt
2025-02-06 17:27:47 [main] INFO com.lee.tcp.ServerDemo - 服务端启动,监听端口:8888
2025-02-06 17:27:54 [pool-1-thread-1] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49521 连接成功
2025-02-06 17:27:57 [pool-1-thread-2] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49525 连接成功
2025-02-06 17:28:04 [pool-1-thread-1] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49521 发送:111
2025-02-06 17:28:07 [pool-1-thread-2] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49525 发送:222
2025-02-06 17:28:12 [pool-1-thread-1] ERROR com.lee.tcp.ReceiverThread - /127.0.0.1:49521 退出了
2025-02-06 17:28:12 [pool-1-thread-1] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49529 连接成功
2025-02-06 17:28:12 [pool-1-thread-1] INFO com.lee.tcp.ReceiverThread - /127.0.0.1:49529 发送:333上面的日志显示了,当连接池被占满后,后续的客户端连接将会被阻塞,直至有空闲线程后才会被处理。所以连接池版本适合HTTP这种请求响应式通信。
5. 响应HTTP请求
HTTP协议是基于TCP构建的,所以我们可以在底层通过响应TCP连接来响应HTTP请求:
java
@Slf4j
public class HttpServerDemo {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888); // 9000会出问题???
System.out.println("Server started on port 8888");
// 创建线程池
ExecutorService pool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2,
Runtime.getRuntime().availableProcessors() * 2,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(8),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
while (true){
// 接受客户端连接
Socket socket = serverSocket.accept();
log.info(socket.getRemoteSocketAddress().toString());
// 向线程池提交任务
pool.submit(new HttpRespondeTask(socket));
}
}
}
@Slf4j
class HttpRespondeTask implements Runnable{
private Socket socket;
public HttpRespondeTask(Socket socket){
this.socket = socket;
}
@Override
public void run() {
try {
// 获取请求头
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requestLine = in.readLine();
log.info(requestLine);
// 以HTTP格式输出内容
OutputStream outputStream = socket.getOutputStream();
PrintWriter printWriter = new PrintWriter(outputStream);
String statusLine = "HTTP/1.1 200 OK\r\n";
String contentType = "Content-Type: text/html\r\n";
String responseBody = "<html><body><h1>Hello, world!</h1></body></html>\r\n";
String response = statusLine + contentType + "\r\n" + responseBody;
printWriter.print(response);
printWriter.flush();
printWriter.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}