


专栏:JavaEE初阶起飞计划
个人主页:手握风云
目录
[1.1. ServerSocket](#1.1. ServerSocket)
[1.2. Socket](#1.2. Socket)
[1.3. 示例](#1.3. 示例)
一、TCP流套接字编程
1.1. ServerSocket
ServerSocket是创建TCP服务端Socket的API。
- ServerSocket构造方法
|------------------------|----------------------------|
| 方法签名 | 方法说明 |
| ServerSocket(int port) | 创建一个服务端流套接字Socket,并绑定到固定端口 |
- ServerSocket方法
|-----------------|---------------------------------------------------------------|
| 方法签名 | 方法说明 |
| Socket accept() | 开始监听指定端口,有客户端连接后,返回一个服务端Socket对象,并基于该Socket对象建⽴与客户端的连接,否则阻塞等待 |
| void close() | 关闭此套接字 |
1.2. Socket
Socket是客户端Socket,或服务端中接收到客户端建立连接(accept⽅法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
- Socket构造方法
|-------------------------------|-------------------------------------------|
| 方法签名 | 方法说明 |
| Socket(String host, int port) | 创建⼀个客户端流套接字Socket,并与对应IP的主机 上,对应端口的进程建立连接 |
- Socket方法
|--------------------------------|-------------|
| 方法签名 | 方法说明 |
| InetAddress getInetAddress() | 返回套接字所连接的地址 |
| InputStream getInputStream() | 返回此套接字的输⼊流 |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
1.3. 示例
TCP是有连接的,不能一上来就读取数据,需要先处理连接。建立连接的过程,是在操作系统内核里已经完成了,只需要在代码中把操作系统里建立好的连接拿过来用就行。
SeverSocket是服务端专用的套接字 ,仅用于在服务端监听客户端的连接请求,是连接的 "管理者"。Socket是通信的端点,既可以作为客户端的套接字(主动发起连接),也可以作为服务端接受连接后生成的套接字(用于与客户端实际通信)。
java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class EchoServer {
private ServerSocket serverSocket;
public EchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 如果客户端没有建立连接就会阻塞
Socket socket = serverSocket.accept();
processConnection(socket);
}
}
/**
* 处理客户端连接的方法
* @param socket 客户端套接字对象,用于与客户端进行通信
*/
private void processConnection(Socket socket) {
System.out.printf("[%s:%d] 客户端上线!\n", socket.getInetAddress().toString(), socket.getPort());
try (InputStream inputStream = socket.getInputStream(); // 从socket获取输入流,用于读取客户端发送的数据
OutputStream outputStream = socket.getOutputStream()) {
Scanner in = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 读取请求并解析
if (!in.hasNext()) {
// 针对客户端下线的逻辑,比如客户端结束了
System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress().toString(), socket.getPort());
break;
}
String request = in.next();
// 根据请求计算响应
String response = process(request);
// 把响应写回到客户端
writer.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 处理请求字符串的方法
* 该方法接收一个字符串参数,直接返回该参数
*
* @param request 需要处理的字符串请求
* @return 返回与输入相同的字符串
*/
private String process(String request) {
// 直接返回输入的请求字符串
return request;
}
public static void main(String[] args) throws IOException {
EchoServer server = new EchoServer(9090);
server.start();
}
}
我们前面也写过一个UDP服务器,两个服务器端口号一样,即使同时启动,也不会产生冲突,因为两个服务器的协议不同。
java
import java.net.Socket;
public class EchoClient {
private Socket socket;
public EchoClient(String severIP, int serverIP) {
socket = new Socket();
}
}
这里的构造方法与UDP不同的是,因为UDP协议本身是无连接的,不记录对端的信息。而TCP是一上来需要连接的,但连接的过程在操作系统内核完成的,但还是需要告诉操作系统服务器的端口和IP是什么。
java
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
/**
* @author gao
* @date 2025/8/21 15:28
*/
public class EchoClient {
private Socket socket;
public EchoClient(String severIP, int serverPort) throws IOException {
socket = new Socket(severIP, serverPort);
}
public void start() {
System.out.println("客户端启动!");
Scanner in = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
Scanner scannerNet = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true) {
// 从控制台读取用户的输入
System.out.print("> ");
String request = in.next();
// 构造请求发送给服务器
writer.println(request);
// 读取服务器的响应
if (!scannerNet.hasNext()) {
System.out.println("服务器断开了连接!");
break;
}
String response = scannerNet.next();
// 响应显示到控制台上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
EchoClient client = new EchoClient("127.0.0.1", 9090);
client.start();
}
}
虽然服务器和客户端都写完了,但我们一启动并输入之后,就会发现,服务器这边并没有任何响应。


这时我们要分析到底是客户端没把数据发送出去,还是服务器收到了没有正确处理。我们可以在服务器代码里面的阻塞后面加上个打印,再运行观察,还是没有。说明上面的hasNext()没有解除阻塞,大概率就是客户端没发来数据。

java
// 发送数据的代码
writer.println(request);
第一个问题,此处的代码执行到了,但是此处的println只是写到了缓冲区,没有写到网卡,也就没有真正发送。缓冲区,英文名称"buffer",通常情况下就是一个"内存空间",计算机读取内存比读取网卡要快很多。假设要很多次写入,就要把多次的数据一次写入网卡。如果缓冲区满了,就会自动传到网卡,或者刷新缓冲区以强行切入到外设。
java
while (true) {
// 读取请求并解析
if (!in.hasNext()) {
// 针对客户端下线的逻辑,比如客户端结束了
System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress().toString(), socket.getPort());
break;
}
System.out.println("服务器收到了数据");
String request = in.next();
// 根据请求计算响应
String response = process(request);
// 把响应写回到客户端
writer.println(response);
writer.flush();
}
java
while (true) {
// 从控制台读取用户的输入
System.out.print("> ");
String request = in.next();
// 构造请求发送给服务器
writer.println(request);
writer.flush();
// 读取服务器的响应
if (!scannerNet.hasNext()) {
System.out.println("服务器断开了连接!");
break;
}
String response = scannerNet.next();
// 响应显示到控制台上
System.out.println(response);
}
第二个问题,当前程序中,存在"文件泄露"。每循环一次,就会触发一次打开文件的操作。一个服务器,不知道要处理多少个客户端,导致打开操作频繁。我们只需要在processConnection最后加上finally代码块。
java
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 如果客户端没有建立连接就会阻塞
Socket socket = serverSocket.accept();
processConnection(socket);
}
}
java
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
但在UDP中的socket对象只有一个,不会频繁创建销毁,生命周期很长,只要1服务器运行,就随时能使用。
第三个问题,程序运行效果的问题。如果在启动多个客户端的情况下就会出现只有一个客户端有连接。在IntelliJ IDEA中要想启动多个实例,需要设置一下。下拉右上角的框,点击"Edit Configurations",再点击Modify options,再勾选上"Allow multiple instances"。



这样我们就可以启动两个客户端,当我们在第一个客户端里面输入任意内容会得到返回结果,但第二个客户端输入内容就没有结果。


这是因为服务器里面的start方法里面有一层循环,而start方法里面的porcessConnection方法里面还有一层循环。只要第一个客户端不退出,processConnection就不会退出。而UDP里面只有一层循环就不会出现上述问题。
我们可以利用之前的多线程知识,把porcessConnection方法放在其他线程里面。
java
public void start() throws IOException {
System.out.println("服务器启动!");
while (true) {
// 如果客户端没有建立连接就会阻塞
Socket socket = serverSocket.accept();
Thread t = new Thread(() ->{
processConnection(socket);
});
t.start();
}
}


其实多线程最初被发明出来的概念就是为了给每个客户端分配线程,由这个线程负责客户端的请求和响应。