前言
在上一节内容中,我们介绍了什么是套接字,以及使用UDP数据报套接字网络编程, 最后我们还介绍了Java数据报套接字通信模型以及相关代码实例。在这一节我们将会介绍TCP流套接字编程。
一、流套接字及通信模型
1.1 TCP套接字
TCP,即Transmission Control Protocol(传输控制协议),是传输层协议。
TCP主要特点:(会在后续单独章节中详细介绍)
- 有连接
- 可靠传输
- 面向字节流
- 有接收、发送缓冲区
- 大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
1.2 原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据。
1.3 Java流套接字通信模型
注意事项:
- 客户端和服务端:开发时,经常是基于一个主机开启两个进程作为客户端和服务端,但真实的场景,一般都是不同主机。
- 注意目的IP和目的端口号,标识了一次数据传输时要发送数据的终点主机和进程
- Socket编程我们是使用流套接字和数据报套接字,基于传输层的TCP或UDP协议,但应用层协议也需要考虑。
- 端口占用问题:如果一个进程A已经绑定了一个端口,再启动一个进程B绑定该端口,就会报错,这种情况也叫端口被占用。
- 解决端口被占用的问题:
如果占用端口的进程A不需要运行,就可以关闭A后,再启动需要绑定该端口的进程B;
如果需要运行A进程,则可以修改进程B的绑定端口,换为其他没有使用的端口。
二、TCP流套接字编程
2.1 ServerSocket API
ServerSocket 是创建TCP服务端Socket的API,ServerSocket构造方法如下:
另外,ServerSocket 相关方法如下:
2.2 Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
Socket的构造方法如下:
Socket相关方法:
2.3 TCP中的长短连接
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,长连接可以多次收发数据。
TCP长短连接对比:
- 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
- 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
三、代码实例
3.1 TCP 客户端
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.ref.SoftReference;
import java.net.Socket;
import java.util.Scanner;
/**
* @author Zhang
* @date 2024/5/2115:13
* @Description:
*/
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIP,int serverPort) throws IOException {
// 此时,需要在创建 Socket 的同时,和服务器”建立连接“,此时就得告诉 Socket 服务器在哪里
socket = new Socket(serverIP,serverPort);
}
public void start(){
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
PrintWriter writer = new PrintWriter(outputStream);
Scanner scannerNetwork = new Scanner(inputStream);
while (true){
// 1. 从控制台读取用户输入的内容
System.out.print("-->");
String request = scanner.next();
// 2. 把字符串作为请求,发给服务器
// 这里用println,是为了让后后面请求带上换行,也就是和服务器读取请求,scanner.next 呼应
writer.println(request);
writer.flush();
// 3. 读取服务器返回的响应
String response = scannerNetwork.next();
// 4. 在界面上显示内容
System.out.println(response);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
}
3.2 TCP 服务端
package network;
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;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author Zhang
* @date 2024/5/2115:13
* @Description:
*/
public class TcpEchoServer {
private ServerSocket serverSocket = null;
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);// 指定端口号
}
public void start() throws IOException {
System.out.println("服务器启动!");
ExecutorService service = Executors.newCachedThreadPool(); //创建线程池
while (true){
//通过 accept 方法,把内核中已经建立好的连接那回到应用程序中
Socket clientSocket = serverSocket.accept();
/**
* 此处不能直接调用processConnection,这会导致服务器不能处理多个客户端,通过创建新的线程来调用是更合理的方法
* Thread t = new Thread(()->{
* try {
* processConnection(clientSocket);
* } catch (IOException e) {
* throw new RuntimeException(e);
* }
* });
* t.start();
*/
//使用创建线程的方式开销会比较大,此处我们使用线程池的方式
service.submit(new Runnable() {
@Override
public void run() {
try {
processConnection(clientSocket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
}
}
// 通过这个方法,来处理当前的连接
private void processConnection(Socket clientSocket) throws IOException {
//进入方法,先打印一个日志
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
//InputStream 和OutputStream就是在字节流,可以借助这两个对象,完成数据的“发送”和“接收”
//通过InputStream 进行read 操作,就是“接收”操作(站在CPU角度)
//通过OutputStream 进行 writ额操作,就是“发送”操作。
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//使用try()方式,避免后续使用完了流对象,忘记关闭
//由于客户端发来的可能是“多条数据”,针对对条数据,进行循环处理
while (true){
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()){
//连接断开了,此时循环就应该结束
System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());
break;
}
// 1. 读取请求并解析, 此处就以next来作为读取请求的方式,next 的规则:读取到“空白符”就返回。
String request = scanner.next();
// 2. 根据请求,计算响应
String response = process(request);
//3. 把响应写回客户端
// 方式一:可以把String转成字符数组,写入到OutputStream
// 方式二:使用PrintWriter 把OutputStream 包裹一下,来写入字符串
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.println(response); //此处的打印是写入到outputStream 对应的流对象中,也就是写入到 clientSocket
printWriter.flush(); //刷新缓冲区,如果没有此操作,数据仍然可能在内存中,没有被写入网卡
// 4.打印一下这次请求交互的内容
System.out.printf("[%s:%d] req=%s resp=%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);
}
}catch (IOException e){
e.printStackTrace();
}finally {
//在这个地方进行clientSocket的关闭,processConnection就是处理一个连接,如果这个方法执行完毕,这个连接也就处理完了
clientSocket.close();
}
}
public String process(String request){
// 此处也是写的服务器,响应和请求一样
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
启动客户端和服务器
运行结果:
//客户端发送请求
TcpEchoClient:
-->张三
张三
-->
----------------------------------
//服务器响应
TcpEchoServer:
服务器启动!
[/127.0.0.1:60107] 客户端上线
[/127.0.0.1:60107] req=张三 resp=张三