当前位置: 首页 > news >正文

用TCP实现服务器与客户端的交互

目录

 一、TCP的特点

二、API介绍 

1.ServerSocket

 2.Socket

三、实现服务器

四、实现客户端

五、测试解决bug

1.客户端发送了数据之后,并没有响应

2.clientSocket没有执行close()操作

3.尝试使用多个客户端同时连接服务器

六、优化

1.短时间有大量客户端访问并断开连接

2.有大量的客户端长时间在线访问

七、源码


引言:

这篇文章主要是用TCP构造的回显服务器,也就是客户端发什么,就返回什么。用实现这个过程方式来学会TCP套接字的使用。

 一、TCP的特点

  • TCP是可靠的:这个需要去了解TCP的机制,这是一个大工程,博主后面写好了把连接附上
  • TCP是面向字节流
  • TCP是全双工
  • TCP是有连接

除了可靠性,在编程中无法体会到,其他特性我都会一 一讲解。

二、API介绍 

1.ServerSocket

ServerSocket 是创建TCP服务端Socket的API

ServerSocket 构造⽅法:

方法签名方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket ⽅法:
⽅法签名
⽅法说明
Socket accpet()开始监听指定端口(创建时绑定端口),有客户端连接后,返回一个服务端Socket对象,并基于Socket建立与客户端的连接,否则阻塞等待
void close()

关闭此套接字

 2.Socket

Socket 是客⼾端Socket,或服务端中接收到客⼾端建⽴连接(accept⽅法)的请求后,返回的服务端Socket。
不管是客⼾端还是服务端Socket,都是双⽅建⽴连接以后,保存的对方信息,及⽤来与对⽅收发数据的

 Socket构造方法:

方法签名方法说明
Socket(String host, int port)

创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接。

host:IP地址

prot:端口号

 这里new出来就是和对方建立完成了。如果建立失败,就会在构造对象的时候抛出异常。

Socket方法:

方法签名方法说明
InetAddress getInetAddress()
返回套接字所连接的地址
InputStream getInputStream()
返回此套接字的输⼊流
OutputStream getOutputStream()
返回此套接字的输出流

三、实现服务器

服务器需要指定端口号:

public class TcpEchoServer {private ServerSocket serverSocket = null;//               需要指定服务器的端口    处理ServerSocket抛出的异常public TcpEchoServer(int port) throws IOException {//                     指定服务器的端口serverSocket = new ServerSocket(port);}
}

注意处理抛出的异常

和客户端建立连接:

public class TcpEchoServer {private ServerSocket serverSocket = null;//               需要指定服务器的端口    处理ServerSocket抛出的异常public TcpEchoServer(int port) throws IOException {//                     指定服务器的端口serverSocket = new ServerSocket(port);}public void start() throws IOException {//服务器需要不停的执行while (true) {//开始监听指定端口,当有客户端连接后,返回一个保存对方信息的SocketSocket clientSocket = serverSocket.accept();//处理逻辑processConnection(clientSocket);}}//针对一个连接,提供处理逻辑private void processConnection(Socket clientSocket) {}
}

这里的accept()就体现了TCP的有连接

当连接成功后,需要处理的逻辑:

    //针对一个连接,提供处理逻辑private void processConnection(Socket clientSocket) {//打印客户端的信息                               返回IP地址                      返回端口号System.out.printf("[%s : %d]客户端上线\n",clientSocket.getInetAddress(), clientSocket.getPort());//获取到socket中持有的流对象try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {while (true) {//1.获取请求//2.处理请求//3.返回响应//4.打印日志}}catch (IOException e) {}}

全双工的意思:通信的双方(如客户端和服务器)可以在同一时间内同时进行数据的发送和接收,即两个方向的数据流可以同时传输,互不干扰。

这里的getInputStream、getOutputStream就体现了全双工和面向字节流。

不了解这两个接口的可以去看我这篇文章:

JAVA如何操作文件?(超级详细)_java操作文件-CSDN博客

实现处理逻辑:

    //针对一个连接,提供处理逻辑private void processConnection(Socket clientSocket) {//打印客户端的信息                               返回IP地址                      返回端口号System.out.printf("[%s : %d]客户端上线\n",clientSocket.getInetAddress(), clientSocket.getPort());//获取到socket中持有的流对象try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {//因为我们用字符串来做为数据传输,用Scanner就可以更方便的传输了Scanner scanner = new Scanner(inputStream);//包装输出流,主要是用println()会在数据之后加上\nPrintWriter printWriter = new PrintWriter(outputStream);while (true) {//1.获取请求if (!scanner.hasNext()) {//如果scanner无法读取出数据,说明客户端断开了连接,导致服务器这边读取到”末尾“break;}//2.处理请求//接收客户端的请求//如果遇到 空白字符 就会停止输入String request = scanner.next();//处理请求String response = process(request);//3.返回响应//此处可以按字节数组的形式,但是我们要输入的是字符串,这个就不太方便//outputStream.write(response.getBytes());//此方法在写入之后会自动加上\nprintWriter.println(response);//4.打印日志System.out.printf("[%s : %d] 请求 = %s 响应 = %s \n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch (IOException e) {throw new RuntimeException();}}private String process(String request) {//由于我们是回显服务器这里直接返回就可以了return request;}

注意里面使用了两个接口包装了一下输入输出流,最主要的是可以在用\n做为分割。

注意里面的:

发送字符串给客户端,最后会自动加上 \n 做为结尾

println(response);

接收客户端信息,以空白符做为结尾。

空白符:包括不限于 空格、回车、制表符……

scanner.next();

如果是nextLine()就比较严格,必须是\n做为结尾

这里的服务器处理逻辑就写完了,但其实这里还有三个错误,后面再单独讲解:

public class TcpEchoServer {private ServerSocket serverSocket = null;//               需要指定服务器的端口    处理ServerSocket抛出的异常public TcpEchoServer(int port) throws IOException {//                     指定服务器的端口serverSocket = new ServerSocket(port);}public void start() throws IOException {//服务器需要不停的执行while (true) {//开始监听指定端口,当有客户端连接后,返回一个保存对方信息的SocketSocket clientSocket = serverSocket.accept();//处理逻辑processConnection(clientSocket);}}//针对一个连接,提供处理逻辑private void processConnection(Socket clientSocket) {//打印客户端的信息                               返回IP地址                      返回端口号System.out.printf("[%s : %d]客户端上线\n",clientSocket.getInetAddress(), clientSocket.getPort());//获取到socket中持有的流对象try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {//因为我们用字符串来做为数据传输,用Scanner就可以更方便的传输了Scanner scanner = new Scanner(inputStream);//包装输出流,主要是用println()会在数据之后加上\nPrintWriter printWriter = new PrintWriter(outputStream);while (true) {//1.获取请求if (!scanner.hasNext()) {//如果scanner无法读取出数据,说明客户端断开了连接,导致服务器这边读取到”末尾“break;}//2.处理请求//接收客户端的请求//如果遇到 空白字符 就会停止输入String request = scanner.next();//处理请求String response = process(request);//3.返回响应//此处可以按字节数组的形式,但是我们要输入的是字符串,这个就不太方便//outputStream.write(response.getBytes());//此方法在写入之后会自动加上\nprintWriter.println(response);//4.打印日志System.out.printf("[%s : %d] 请求 = %s 响应 = %s \n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch (IOException e) {throw new RuntimeException();}}private String process(String request) {//由于我们是回显服务器这里直接返回就可以了return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(8080);server.start();}
}

四、实现客户端

指定服务器的IP和端口号:

public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String serverIP, int serverPort) throws IOException {//这里只要建立实例,就是和服务端的accept()建立了连接//socket也就保存了服务器的IP和端口号等//需要传入服务器的 IP地址 和 端口号socket = new Socket(serverIP, serverPort);}public void start() {System.out.println("客户端启动!");}}

整体逻辑:

public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String serverIP, int serverPort) throws IOException {//需要传入服务器的 IP地址 和 端口号//这里只要建立实例,就是和服务端的accept()建立了连接socket = new Socket(serverIP, serverPort);}public void start() {System.out.println("客户端启动!");try(OutputStream outputStream = socket.getOutputStream();InputStream inputStream = socket.getInputStream()) {while (true) {//1.从控制台获取数据//2.将数据发送给服务器//3.接收服务器响应//4.打印相关结果}}catch (IOException e) {throw new RuntimeException();}}
}

整体逻辑实现:

public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String serverIP, int serverPort) throws IOException {//这里只要建立实例,就是和服务端的accept()建立了连接//socket也就保存了服务器的IP和端口号等//需要传入服务器的 IP地址 和 端口号socket = new Socket(serverIP, serverPort);}public void start() {System.out.println("客户端启动!");try(OutputStream outputStream = socket.getOutputStream();InputStream inputStream = socket.getInputStream()) {//用来接收服务器的信息Scanner scanner = new Scanner(inputStream);//用于接收用户输入Scanner scannerIn = new Scanner(System.in);//用于输出数据给服务器PrintWriter printWriter = new PrintWriter(outputStream);while (true) {//1.从控制台获取数据System.out.print("->");String request = scannerIn.next();//2.将数据发送给服务器printWriter.println(request);//3.接收服务器响应//判断服务端是否还有信息if (!scanner.hasNext()) {break;}//接收服务端信息String response = scanner.next();//4.打印相关结果System.out.println(response);}}catch (IOException e) {throw new RuntimeException();}}public static void main(String[] args) throws IOException {
//                                              127.0.0.1是专门用来访问自己的TcpEchoClient client = new TcpEchoClient("127.0.0.1",8080);client.start();}
}

这里仍然纯在一个问题,一会和服务器的问题一起将

五、测试解决bug

最后我会把所有的问题解决了,再把源附上

1.客户端发送了数据之后,并没有响应

 先运行服务器,再运行客户端

可以看到目前还是成功的,那么我们来输入数据。

我们在客户端输入了消息,但是没有任何反应了!

此处的情况是,客户端并没有真正把请求发出去:

PrintWriter这样的类,以及很多IO流中的类,都是 “自带缓冲区” 的。

此方法就带有缓冲区:

printWriter.println(request);

引入缓冲区之后,进行写入数据的操作,并不会马上触发IO,而是先放到内存缓冲区中,等到缓冲区里攒了一波之后,再统一进行发送。

为什么引入缓冲区的机制?

因为IO操作其实是不小的开销,如果数据量较少,那么每一次都进行IO,就有很大一部分开销是IO操作。如果积累到一定数据量再进行IO操作,那么一次IO就传输了这么多数据。

我们可以使用flush方法,主动“刷新缓冲区”:

注意:

服务器 和 客户端 都需要在printWriter.println();后面加上flush()方法。

再来测试:

此时就可以接收到了

2.clientSocket没有执行close()操作

这个问题比较隐蔽,这些ServerSocket 和 Socket 每一次都会在“文件描述符”中创建一个新的表项。

文件描述符:描述了该进程都要操作哪些文件。数组的每个元素就是一个struct file对象,每个结构体就描述了对应的文件信息,数组的小标就称为“文件描述符”。

每次打开一个文件,就想当于在数组上占用了一个位置,而这个数组又是不能扩容的,如果数组满了就会打开文件失败。除非主动调用close才会关闭文件,或者这个进程直接结束了这个数组也被带走了。

那么我们就需要处理一下clientSocket:

 3.尝试使用多个客户端同时连接服务器

要对同一代码启动多个进程,需要设置一下步骤:

分别启动客户端1 和 客户端2 ,可以看到服务器上根本没有第二个客户端启动的信息:

原因:

我们可以用多线程去执行专门执行每一个客户端的请求:

    public void start() throws IOException {//服务器需要不停的执行while (true) {//开始监听指定端口,当有客户端连接后,返回一个保存对方信息的SocketSocket clientSocket = serverSocket.accept();//让一个线程去对应一个客户端Thread thread = new Thread(() -> {//处理逻辑processConnection(clientSocket);});thread.start();}}

结果:

bug问题解决了,但还有一些场景,可能会把服务器干崩溃

六、优化

1.短时间有大量客户端访问并断开连接

一旦短时间内有大量的客户端,并且每个客户端请求都是很快的连接之后并退出的,这个时候对于服务器来说,就会有比较大的压力。这个时候,就算是进程比线程更加的轻量,但是短时间内有大量的线程创建销毁,就无法忽略它的开销了。

我们可以引入线程池,这样就解决了这个问题:

    public void start() throws IOException {//服务器需要不停的执行while (true) {//开始监听指定端口,当有客户端连接后,返回一个保存对方信息的SocketSocket clientSocket = serverSocket.accept();ExecutorService service = Executors.newCachedThreadPool();//            //让一个线程去对应一个客户端
//            Thread thread = new Thread(() -> {
//                //处理逻辑
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            thread.start();service.submit(() -> {try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}});}}

这个线程可创建的线程数是很大的:

2.有大量的客户端长时间在线访问

例如直播这样的情况,每个客户端分配一个线程,对于一个系统来说,这里搞几百个线程压力就非常大了。所以这里 线程池/线程 都不太适用了。

可以使用 IO多路复用 ,也就是一个线程分配多个客户端进行服务,因为大部分时间线程都是在等待状态,就能够让线程分配多个客户端,这样的机制我们做为java程序员不需要过多了解,这样的机制以及被大佬们,装进各种框架中了。

七、源码

服务器源码:

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;public class TcpEchoServer {private ServerSocket serverSocket = null;//               需要指定服务器的端口    处理ServerSocket抛出的异常public TcpEchoServer(int port) throws IOException {//                     指定服务器的端口serverSocket = new ServerSocket(port);}public void start() throws IOException {//服务器需要不停的执行while (true) {//开始监听指定端口,当有客户端连接后,返回一个保存对方信息的SocketSocket clientSocket = serverSocket.accept();ExecutorService service = Executors.newCachedThreadPool();//            //让一个线程去对应一个客户端
//            Thread thread = new Thread(() -> {
//                //处理逻辑
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            thread.start();service.submit(() -> {try {processConnection(clientSocket);} catch (IOException e) {e.printStackTrace();}});}}//针对一个连接,提供处理逻辑private void processConnection(Socket clientSocket) throws IOException {//打印客户端的信息                               返回IP地址                      返回端口号System.out.printf("[%s : %d]客户端上线\n",clientSocket.getInetAddress(), clientSocket.getPort());//获取到socket中持有的流对象try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()) {//因为我们用字符串来做为数据传输,用Scanner就可以更方便的传输了Scanner scanner = new Scanner(inputStream);//包装输出流,主要是用println()会在数据之后加上\nPrintWriter printWriter = new PrintWriter(outputStream);while (true) {//1.获取请求if (!scanner.hasNext()) {//如果scanner无法读取出数据,说明客户端断开了连接,导致服务器这边读取到”末尾“break;}//2.处理请求//接收客户端的请求//如果遇到 空白字符 就会停止输入String request = scanner.next();//处理请求String response = process(request);//3.返回响应//此处可以按字节数组的形式,但是我们要输入的是字符串,这个就不太方便//outputStream.write(response.getBytes());//此方法在写入之后会自动加上\nprintWriter.println(response);//刷新缓冲区printWriter.flush();//4.打印日志System.out.printf("[%s : %d] 请求 = %s 响应 = %s \n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}}catch (IOException e) {throw new RuntimeException();} finally {System.out.printf("[%s : %d]客户端下线\n",clientSocket.getInetAddress(), clientSocket.getPort());clientSocket.close();}}private String process(String request) {//由于我们是回显服务器这里直接返回就可以了return request;}public static void main(String[] args) throws IOException {TcpEchoServer server = new TcpEchoServer(8080);server.start();}
}

客户端源码:

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 {//这里只要建立实例,就是和服务端的accept()建立了连接//socket也就保存了服务器的IP和端口号等//需要传入服务器的 IP地址 和 端口号socket = new Socket(serverIP, serverPort);}public void start() {System.out.println("客户端启动!");try(OutputStream outputStream = socket.getOutputStream();InputStream inputStream = socket.getInputStream()) {//用来接收服务器的信息Scanner scanner = new Scanner(inputStream);//用于接收用户输入Scanner scannerIn = new Scanner(System.in);//用于输出数据给服务器PrintWriter printWriter = new PrintWriter(outputStream);while (true) {//1.从控制台获取数据System.out.print("->");String request = scannerIn.next();//2.将数据发送给服务器printWriter.println(request);//刷新缓冲区printWriter.flush();//3.接收服务器响应//判断服务端是否还有信息if (!scanner.hasNext()) {break;}//接收服务端信息String response = scanner.next();//4.打印相关结果System.out.println(response);}}catch (IOException e) {throw new RuntimeException();}}public static void main(String[] args) throws IOException {TcpEchoClient client = new TcpEchoClient("127.0.0.1",8080);client.start();}
}


http://www.mrgr.cn/news/100818.html

相关文章:

  • 新闻客户端案例的实现,使用axios获取数据并渲染页面,路由传参(查询参数,动态路由),使用keep-alive实现组件缓存
  • MySQL基础关键_002_DQL(一)
  • string类(详解)
  • ESP32开发-作为TCP服务端接收数据
  • 分组密码算法ShengLooog设计原理详解
  • conda管理python环境
  • ZYNQ笔记(十四):基于 BRAM 的 PS、PL 数据交互
  • 快速搭建对象存储服务 - Minio,并解决临时地址暴露ip、短链接请求改变浏览器地址等问题
  • 希尔伯特第十问题:是一个伪命题
  • 服务容错治理框架resilience4jsentinel基础应用---微服务的限流/熔断/降级解决方案
  • C++入门小馆: 模板
  • 【数学建模国奖速成系列】优秀论文绘图复现代码(二)
  • 1295.统计位数为偶数的数字
  • 数据结构篇:线性表的另一表达—链表之单链表(下篇)
  • [CPCTF 2025] Crypto
  • Python os.path.join()路径拼接异常
  • X²+1素数问题
  • 4:机器人目标识别无序抓取程序二次开发
  • 【中间件】bthread效率为什么高?
  • HCIP-数据通信datacom认证