不像 UDP 有 DatagramPacket 是专门的“UDP 数据报”,TCP 没有专门的“TCP 数据报”
专门给服务器使用的 socket 对象
| 方法签名 | 方法说明 | |
|---|---|---|
| ServerSocket(int port) 创建⼀个服务端流套接字 Socket,并绑定到指定端⼝ | 创建⼀个服务端流套接字 Socket,并绑定到指定端⼝ |
| 方法签名 | 方法说明 | |
|---|---|---|
| Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端 Socket 对象,并基于该 Socket 建⽴与客⼾端的连接,否则阻塞等待 | |
| void close() | 关闭此套接字 |
既会给客户端使用,又会给服务器使用
| 方法签名 | 方法说明 | |
|---|---|---|
| Socket(String host, int port) | 创建⼀个客⼾端流套接字 Socket,并与对应 IP 的主机上,对应端⼝的进程建⽴连接 |
| 方法签名 | 方法说明 | |
|---|---|---|
| InetAddress getInetAddress() | 返回套接字所连接的地址 | |
| InputStream getInputStream() | 返回此套接字的输⼊流 | |
| OutputStream getOutputStream() | 返回此套接字的输出流 |
InputStream 和 OutputStream 称为“字节流”
TCP Socket 来说,也是完全适用的Server Socket 对象,起到“遥控网卡”的作用import java.io.IOException; import java.net.ServerSocket; public class TcpEchoServer { private ServerSocket serverSocket= null; public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } } socket 对象创建的时候,就指定一个端口号 port,作为构造方法的参数JVM 就会调用系统的 Socket API,完成“端口号-进程”之间的关联动作 API 名字就叫 bind)Socket 对象来完成) public void start() throws IOException { while(true) { //建立连接 Socket clientSocket = serverSocket.accept(); processConnection(clientSocket); } } accept 操作,是内核已经完成了连接建立的操作,然后才能够进行“接通电话”accept 相当于是针对内核中已经建立好的连接进行“确认”动作accept 的返回对象是 Socket,所以还需要创建一个 clientSocket 来接收返回值 clientSocket 和 serverSocket 这两个都是 Socket,都是“网卡的遥控器”,都是用来操作网卡的。但是在 TCP 中,使用两个不同的 Socket 进行表示,他们的分工是不同的,作用是不同的 serverSocket 就相当于是卖房子的销售,负责在外面揽客clientSocket 相当于是售楼部里面的置业顾问,提供“一对一服务”针对一个连接,提供处理逻辑
InputStream 对象用来读取数据,创建一个 OutputStream 对象while 死循环中完成客户端针对请求的响应处理private void processConnection(Socket clientSocket) { //打印客户端信息 System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort()); try(InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()){ while(true) { // 1. 读取请求并解析 // 2. 根据请求计算响应 // 3. 把响应写回给客户端 } }catch (IOException e){ e.printStackTrace(); } System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress(),clientSocket.getPort()); } TCP 是全双工的通信,所以一个 Socket 对象,既可以读,也可以写clientSocket 对象拿出里面的 InputStream 和 OutputStream,我们就既能读,也能写了通过 inputStream.read() 读取请求,但如果直接这样读就不方便,读到的还是二进制数据
Scanner 包装一下 InputStream,这样就可以更方便地读取这里的请求数据了//针对一个连接,提供处理逻辑 private void processConnection(Socket clientSocket) { //打印客户端信息 System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress(),clientSocket.getPort()); try(InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()){ Scanner scanner = new Scanner(inputStream); //使用 Scanner 包装一下 InputStream,就可以更方便地读取这里的请求数据了 while(true) { // 1. 读取请求并解析 if(!scanner.hasNext()){ //如果 scanner 无法读取数据,说明客户端关闭了连接,导致服务器这边读取到 “末尾” break; } // 2. 根据请求计算响应 // 3. 把响应写回给客户端 } }catch (IOException e){ e.printStackTrace(); } System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress(),clientSocket.getPort()); } scanner 无法读取出数据时(scanner 没有下一个数据了),说明客户端关闭了连接,导致服务器这边读到了末尾,就进行 breaktry/catch 外面)加上日志,当数据读完后 break 了,就打印日志由于是回显服务器,所以请求就是响应,process 就是直接 return request
//针对一个连接,提供处理逻辑 private void processConnection(Socket clientSocket) { //打印客户端信息 System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress(),clientSocket.getPort()); try(InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()){ Scanner scanner = new Scanner(inputStream); //使用 Scanner 包装一下 InputStream,就可以更方便地读取这里的请求数据了 while(true) { // 1. 读取请求并解析 if(!scanner.hasNext()){ //如果 scanner 无法读取数据,说明客户端关闭了连接,导致服务器这边读取到 “末尾” break; } // 2. 根据请求计算响应 String response = process(request); // 3. 把响应写回给客户端 } }catch (IOException e){ e.printStackTrace(); } System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress(),clientSocket.getPort()); private String process(String request) { return request; } } InputStream 里面的数据//针对一个连接,提供处理逻辑 private void processConnection(Socket clientSocket) { //打印客户端信息 System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress(),clientSocket.getPort()); try(InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()){ Scanner scanner = new Scanner(inputStream); //使用 Scanner 包装一下 InputStream,就可以更方便地读取这里的请求数据了 PrintWrite printWriter = new PrintWriter(outputStream); while(true) { // 1. 读取请求并解析 Scanner scanner = new Scanner(inputStream); if(!scanner.hasNext()){ //如果 scanner 无法读取数据,说明客户端关闭了连接,导致服务器这边读取到 “末尾” break; } // 2. 根据请求计算响应 String response = process(request); // 3. 把响应写回给客户端 printWriter.println(response); } }catch (IOException e){ e.printStackTrace(); } System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress(),clientSocket.getPort()); private String process(String request) { return request; } } \n” scanner 读取请求的时候,隐藏了一个条件——请求是以“空白符”(空格、回车、制表符、垂直制表符、翻页符…)结尾,否则就会在 next() 或者 hasNext() 那里发生阻塞,这样就没法读取到数据了\n”作为请求和响应的结尾标志TCP 是字节流的,读写方式存在无数种可能,就需要有办法区分出,从哪里到哪里是一个完整的请求 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 TcpEchoServer { private ServerSocket serverSocket= null; public TcpEchoServer(int port) throws IOException { serverSocket = new ServerSocket(port); } public void start() throws IOException { while(true) { //建立连接 Socket clientSocket = serverSocket.accept(); processConnection(clientSocket); } } //针对一个连接,提供处理逻辑 private void processConnection(Socket clientSocket) { //打印客户端信息 System.out.printf("[%s:%d] 客户端上线!",clientSocket.getInetAddress(),clientSocket.getPort()); try(InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()){ Scanner scanner = new Scanner(inputStream); PrintWriter printWriter = new PrintWriter(outputStream); //使用 Scanner 包装一下 InputStream,就可以更方便地读取这里的请求数据了 while(true) { // 1. 读取请求并解析 if(!scanner.hasNext()){ //如果 scanner 无法读取数据,说明客户端关闭了连接,导致服务器这边读取到 “末尾” break; } String request = scanner.next(); // 2. 根据请求计算响应 String response = process(request); // 3. 把响应写回给客户端 printWriter.println(response); System.out.printf("[%s:%d] req=%s; resp=%s\n", clientSocket.getInetAddress(),clientSocket.getPort()); } }catch (IOException e){ e.printStackTrace(); } System.out.printf("[%s:%d] 客户端下线!",clientSocket.getInetAddress(),clientSocket.getPort()); } private String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9090); server.start(); } } 虽然把服务器代码编写的差不多了,但还存在三个非常严重的问题,都会导致严重的 bug
但需要结合后面客户端的代码进行分析
首先创建一个 Socket 对象,来进行网络通信,再创建构造方法
import java.io.IOException; import java.net.Socket; public class TcpEchoClient { private Socket socket = null; public TcpEchoClient(String serverIp, int serverPort) throws IOException { socket = new Socket(serverIp,serverPort); } } IP 和端口号 String 类型的 IP,不用像前面 UDP 那样还需要手动转换socket 里面的 InputStream 和 OutputStream,再进行 while 循环Scanner 包装一下 InputStream,这样就可以更方便地读取这里的请求数据了PrintWriter 对象,获取到 OutputStream,方便后续对数据进行打印scannerIn 对象,用来读取从控制台输入的数据public void start() { System.out.println("客户端启动!"); try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { Scanner scanner = new Scanner(inputStream); Scanner scannerIn = new Scanner(System.in); PrintWriter printWriter = new PrintWriter(outputStream); while(true){ //1. 从控制台读取数据 System.out.println("-> "); String request = scannerIn.next(); //2. 把请求发送给服务器 printWriter.println(request); //3. 从服务器读取响应 if(!scanner.hasNext()){ break; } String response = scanner.next(); //4. 打印响应结果 System.out.println(response); } } catch (Exception e) { throw new RuntimeException(e); } } import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.util.Scanner; public class TcpEchoClient { private Socket socket = null; public TcpEchoClient(String serverIp, int serverPort) throws IOException { socket = new Socket(serverIp,serverPort); } public void start() { System.out.println("客户端启动!"); try(InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { Scanner scanner = new Scanner(inputStream); Scanner scannerIn = new Scanner(System.in); PrintWriter printWriter = new PrintWriter(outputStream); while(true){ //1. 从控制台读取数据 System.out.println("-> "); String request = scannerIn.next(); //2. 把请求发送给服务器 printWriter.println(request); //3. 从服务器读取响应 if(!scanner.hasNext()){ break; } String response = scanner.next(); //4. 打印响应结果 System.out.println(response); } } catch (Exception e) { throw new RuntimeException(e); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090); client.start(); } } //这是客户端中,将数据发送给服务器的代码 printWriter.println(request); //这是服务器中,把响应写回给客户端的代码 printWriter.println(response); PrintWriter 这样的类,以及很多 IO 流中的类,都是“自带缓冲区”的 flush 操作,主动“刷新缓冲区” 改为:
// 客户端 printWriter.println(request); printWriter.flush(); // 服务器 printWriter.println(response); printWriter.flush(); while(true) { //建立连接 Socket clientSocket = serverSocket.accept(); processConnection(clientSocket); } ServerSocket 和 DatagramPacket,它们的生命周期都是跟随整个进程的,和进程同生死,进程关了之后他俩对应的资源也释放了clientSocket 并非如此,它是“连接级别”的数据,随着客户端断开连接了,这个 Socket 也就不再使用了,但资源是不释放的 Socket,和旧的 Socket 不是同一个了Socket 就应该主动关闭掉,避免文件资源泄露改后:
把 close 加到 finally 里面,把日志前移(不然释放之后日志就打印不出来了)
private void processConnection(Socket clientSocket) throws IOException { try(InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()){ ... while(true) { ... } }catch (IOException e){ e.printStackTrace(); }finally { System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort()); clientSocket.close(); } } GC 释放的是内存资源,此处讨论的“文件资源泄露”是针对文件描述符的
GC 回收了,也是会自动执行 close 的,但是由于 GC 过程是不可逆的(不知道 GC 什么时候发生,也不知到这次 GC 是否能释放掉你这个对象)processConnect 内部的 while 循环,无法跳出acceptprocessConnect 的循环就结束了,于是外层的循环就可以执行 accept 了,也是就可以处理第二个客户端之前积压的请求数据了while 循环的写法,导致进入里层 while 的时候,外层 while 就无法执行了while 改成一重 while,分别进行执行——使用多线程改后:
public void start() throws IOException { while(true) { //建立连接 Socket clientSocket = serverSocket.accept(); Thread t = new Thread(() -> { try { processConnection(clientSocket); } catch (IOException e) { throw new RuntimeException(e); } }); t.start(); } }