不像 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
没有下一个数据了),说明客户端关闭了连接,导致服务器这边读到了末尾,就进行 break
try/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
循环,无法跳出accept
processConnect
的循环就结束了,于是外层的循环就可以执行 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(); } }