【JavaEE】网络编程套接字2: TCP流 套接字编程
一、基于TCP的Socket API
首先要明确 TCP 协议和 UDP 协议的很重要的区别 :
TCP 协议是有链接, 面向字节流传输。
主要体现在 : 发送方和接收方在网络通信之间要先建立连接, 并且传输的数据的基本单位是字节。
1.1 基于 TCP 协议的 Socket API 中, 要分清楚以下两个类
类名 | 解释 |
---|---|
ServerSocket | 创建TCP服务端Socket的API |
Socket | Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept 方法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。 |
(1)ServerSocket 类:
- 创建一个这样的对象,就相当于打开了一个 socket 文件。
- 这个 socket 对象是给服务器专门使用的。
- 这个类本身不负责发送、接收,主要负责“建立连接”。
(2)Socket 类:
- 创建这样一个对象,也就相当于打开了一个 socket 文件。
- 这个类,服务器和客户端都会使用。
- 这个类,负责发送、接收数据。
(3)TCP 是字节流的,读写的时候,都以字节 byte 为基本单位。而文件也是字节流的,读写 TCP 的代码,本质上就是和读写文件的代码是一致的,都是通过 InputStream / OutputStream 展开的。
1.2 ServerSocket 类 和 Socket 类 的构造方法和成员方法
1️⃣ServerSocket 类
1)ServerSocket 类的构造方法 :
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 创建⼀个服务端流套接字Socket,并绑定到指定端⼝ |
2)ServerSocket 类的成员方法 :
方法签名 | 方法说明 |
---|---|
Socket accept() | 开始监听指定端⼝(创建时绑定的端⼝),有客⼾端连接后,返回⼀个服务端Socket对象,并基于该Socket 建⽴与客⼾端的连接,否则阻塞等待。 |
void close() | 关闭此套接字 |
2️⃣Socket 类
1)Socket 类的构造方法 :
方法签名 | 方法说明 |
---|---|
ServerSocket(int port) | 建⼀个客户端流套接字Socket,并与对应IP的主机上,对应端⼝的进程建⽴连接 |
2)Socket 类的成员方法 :
方法签名 | 方法说明 |
---|---|
InetAddress getInetAddress() | 返回套接字所连接的地址 |
IInputStream getInputStream() | 返回此套接字的输⼊流 |
OutputStream getOutputStream() | 返回此套接字的输出流 |
二、TCP流 套接字编程
2.1 服务器
package network.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;
//TCP的回显服务器
public class EchoServer {
private ServerSocket serverSocket;
public EchoServer(int port) throws IOException{
//服务器启动,就会绑定到 port 这个端口上面
serverSocket = new ServerSocket(port);
}
public void start() throws IOException{
System.out.println("服务器启动!");
while(true){
Socket socket = serverSocket.accept();
processConnection(socket);
}
}
//处理一个客户端/一个连接的逻辑
private void processConnection(Socket socket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n",socket.getInetAddress().toString(),socket.getPort());
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
//实现通信的代码
//一个客户端可能会和服务器有多轮的请求响应交互
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true){
//1.读取请求并解析,这个地方有更简单的方法
//作为回显服务器,客户端给服务器发的都是字符串(字符串,可以按照字符流来处理,比直接按照字节流更方便)
if (!scanner.hasNext()) {
//针对客户端下线逻辑的处理。如果客户端打开连接了(比如客户端结束了)
//此时 hasNext 就会返回 false
//如果使用 read方法,就会出现返回-1的情况,也可以用来判断客户端断开连接
System.out.printf("[%s:%d] 客户端下线!\n",socket.getInetAddress(),socket.getPort());
break;
}
String request = scanner.next();
/*byte[] buffer = new byte[1024];
inputStream.read(buffer);*/
//2.根据请求计算响应
String response = process(request);
//3.把响应写回到客户端
writer.println(response);
System.out.printf("[%s:%d] req: %s; resq: %s\n",socket.getInetAddress(),socket.getPort(),
request,response);
}
}catch (IOException e){
e.printStackTrace();
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
EchoServer echoServer = new EchoServer(9090);//虽然前面的UDP和这里的TCP,端口都是9090
//但是二者不会冲突,因为协议不同
echoServer.start();
}
}
2.2 客户端
package network.TCP;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
//TCP的回显客户端
public class EchoClient {
private Socket socket;
public EchoClient(String serverIp,int serverPort) throws IOException {
//在 new 这个对象的时候,就涉及到“建立连接操作”
//由于连接建立好了之后,服务器的信息就在操作系统中被 TCP 协议记录了。我们在应用程序层面上就不需要保存 IP 和 端口
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("启动客户端!");
Scanner scanner = 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){
//1.从控制台读取用户的输入
System.out.println("> ");
String request = scanner.next();
//2.构造请求发送给服务器
writer.println(request);
//3.读取服务器的响应
if(!scannerNet.hasNext()){
System.out.println("服务器断开了连接");
break;
}
String response = scannerNet.next();
//4.把响应显示到控制台上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
EchoClient echoClient = new EchoClient("127.0.0.1",9090);
echoClient.start();
}
}
2.3 上述代码 存在的问题
运行网络程序,一定是先启动服务器,后启动客户端~
1️⃣ 问题1:通信不通(客户端的2,直接调用 println只是把数据写入缓冲区,并没有真正进入网卡)
1)现象
客户端发了请求之后,没有收到响应。
(是客户端没把数据发出去?还是服务器收到了没有正确处理?)
先启动服务器,输出“服务器启动”,显示“客户端上线!”; Ctrl+F2结束客户端的运行,显示“客户端下线!”; 重新运行服务器,键盘输入hello,什么都不显示。
2)解决方案:加上刷新缓冲区 writer.flush()
① 客户端
package network.TCP;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
//TCP的回显客户端
public class EchoClient {
private Socket socket;
public EchoClient(String serverIp,int serverPort) throws IOException {
//在 new 这个对象的时候,就涉及到“建立连接操作”
//由于连接建立好了之后,服务器的信息就在操作系统中被 TCP 协议记录了。我们在应用程序层面上就不需要保存 IP 和 端口
socket = new Socket(serverIp,serverPort);
}
public void start(){
System.out.println("启动客户端!");
Scanner scanner = 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){
//1.从控制台读取用户的输入
System.out.println("> ");
String request = scanner.next();
//2.构造请求发送给服务器
writer.println(request);//此处的 println 是执行到了
//但是 println只是把数据先写到缓冲区,没有真正得写入网卡,也就没有真正得发送
//缓冲区:buffer,就是一个"内存空间"。假设要频繁得写入网卡,就需要把多次的数据攒一起,一次性写入网卡。
writer.flush();//刷新缓冲区
//3.读取服务器的响应
if(!scannerNet.hasNext()){
System.out.println("服务器断开了连接");
break;
}
String response = scannerNet.next();
//4.把响应显示到控制台上
System.out.println(response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
EchoClient echoClient = new EchoClient("127.0.0.1",9090);
echoClient.start();
}
}
② 服务器
package network.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;
//TCP的回显服务器
public class EchoServer {
private ServerSocket serverSocket;
public EchoServer(int port) throws IOException{
//服务器启动,就会绑定到 port 这个端口上面
serverSocket = new ServerSocket(port);
}
public void start() throws IOException{
System.out.println("服务器启动!");
while(true){
Socket socket = serverSocket.accept();
processConnection(socket);
}
}
//处理一个客户端/一个连接的逻辑
private void processConnection(Socket socket) throws IOException {
System.out.printf("[%s:%d] 客户端上线!\n",socket.getInetAddress().toString(),socket.getPort());
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
//实现通信的代码
//一个客户端可能会和服务器有多轮的请求响应交互
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);
while (true){
//1.读取请求并解析,这个地方有更简单的方法
//作为回显服务器,客户端给服务器发的都是字符串(字符串,可以按照字符流来处理,比直接按照字节流更方便)
if (!scanner.hasNext()) {
//针对客户端下线逻辑的处理。如果客户端打开连接了(比如客户端结束了)
//此时 hasNext 就会返回 false
//如果使用 read方法,就会出现返回-1的情况,也可以用来判断客户端断开连接
System.out.printf("[%s:%d] 客户端下线!\n",socket.getInetAddress(),socket.getPort());
break;
}
//没有执行到这个打印,说明上面的 hasNext 没有解除阻塞,大概率就是客户端没发来数据
//并且没可能丢包:因为是自己给自己发送 (客户端和服务器在一台主机)
System.out.println("服务器收到数据了!");
String request = scanner.next();
/*byte[] buffer = new byte[1024];
inputStream.read(buffer);*/
//2.根据请求计算响应
String response = process(request);
//3.把响应写回到客户端
writer.println(response);
writer.flush();
System.out.printf("[%s:%d] req: %s; resq: %s\n",socket.getInetAddress(),socket.getPort(),
request,response);
}
}catch (IOException e){
e.printStackTrace();
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
EchoServer echoServer = new EchoServer(9090);//虽然前面的UDP和这里的TCP,端口都是9090
//但是二者不会冲突,因为协议不同
echoServer.start();
}
}
③ 输出结果
2️⃣问题2:当前的程序,存在“文件资源泄露”问题
① 问题出现的原因
② 解决方法
③ 为什么只有TCP的socket 需要关闭?
④ 为啥 inputScream 和 outputScream 不需要关闭?
3️⃣问题3:存在“多个客户端”的情况(与程序运行效果有关)
① 现象
首先,先打开两个客户端:
然后观察发现:第二个开启的客户端并没有和服务器成功通信, 这是因为, 我们的服务器处理多个连接时, 是在一个while循环中, 如果第一个连接的客户端没有下线, 就不会接收第二个客户端的连接。
退出第一个客户端,第二个客户端就可以正常使用。
② 出现问题的原因
③解决方法1:多线程
观察发现:多个客户端可以正常通信。
解决方法2:线程池
③ 拓展:IO多路复用
三、TCP回显服务器执行流程
四、完整代码
3.1 客户端
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() {
Scanner in = new Scanner(System.in);
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()) {
// 把字节流转换成字符流
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 发送多个请求
while (true) {
// 1,从控制台输入字符串
String requestString = in.next();
// 2,写入请求
printWriter.println(requestString);
printWriter.flush();
// 3,读取请求
String responseString = inFromSocket.next();
// 控制台 打印请求字符串 + 响应字符串
System.out.println(requestString + " + " + responseString);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
TCPEchoClient tcpEchoClient = new TCPEchoClient("127.0.0.1", 9999);
tcpEchoClient.start();
}
}
3.2 服务器
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
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 对象
Socket socket = serverSocket.accept();
// 处理连接到的这个客户端
Thread thread = new Thread( () -> {
try {
processConnection(socket);
} catch (IOException e) {
e.printStackTrace();
}
});
// 别忘了调用 start() 启动线程
thread.start();
}
}
private void processConnection(Socket socket) throws IOException {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端上线");
try (InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream() ) {
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner inFromSocket = new Scanner(inputStream);
// 处理多个请求
while(true) {
if (!inFromSocket.hasNext()) {
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + "此客户端下线");
break;
}
// 1,读取请求
String requestString = inFromSocket.next();
// 2,处理请求
String responseString = process(requestString);
// 3,写入响应
printWriter.println(responseString);
printWriter.flush();
// 控制台打印 客户端IP地址 + 客户端端口号 + 请求字符串 + 响应字符串
System.out.println(socket.getInetAddress() + " + " + socket.getPort() + " + " + requestString + " + " + responseString);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
socket.close();
}
}
private String process(String requestString) {
return requestString;
}
public static void main(String[] args) throws IOException {
TCPEchoServer tcpEchoServer = new TCPEchoServer(9999);
tcpEchoServer.start();
}
}