JavaEE 初阶第二十期:网络编程“通关记”(二)
专栏:JavaEE初阶起飞计划
个人主页:手握风云
目录
一、TCP流套接字编程
1.1. ServerSocket
1.2. Socket
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是通信的端点,既可以作为客户端的套接字(主动发起连接),也可以作为服务端接受连接后生成的套接字(用于与客户端实际通信)。
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服务器,两个服务器端口号一样,即使同时启动,也不会产生冲突,因为两个服务器的协议不同。
import java.net.Socket;public class EchoClient {private Socket socket;public EchoClient(String severIP, int serverIP) {socket = new Socket();}
}
这里的构造方法与UDP不同的是,因为UDP协议本身是无连接的,不记录对端的信息。而TCP是一上来需要连接的,但连接的过程在操作系统内核完成的,但还是需要告诉操作系统服务器的端口和IP是什么。
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()没有解除阻塞,大概率就是客户端没发来数据。
// 发送数据的代码
writer.println(request);
第一个问题,此处的代码执行到了,但是此处的println只是写到了缓冲区,没有写到网卡,也就没有真正发送。缓冲区,英文名称"buffer",通常情况下就是一个“内存空间”,计算机读取内存比读取网卡要快很多。假设要很多次写入,就要把多次的数据一次写入网卡。如果缓冲区满了,就会自动传到网卡,或者刷新缓冲区以强行切入到外设。
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();
}
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代码块。
public void start() throws IOException {System.out.println("服务器启动!");while (true) {// 如果客户端没有建立连接就会阻塞Socket socket = serverSocket.accept();processConnection(socket);}
}
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方法放在其他线程里面。
public void start() throws IOException {System.out.println("服务器启动!");while (true) {// 如果客户端没有建立连接就会阻塞Socket socket = serverSocket.accept();Thread t = new Thread(() ->{processConnection(socket);});t.start();}
}
其实多线程最初被发明出来的概念就是为了给每个客户端分配线程,由这个线程负责客户端的请求和响应。