实现一个TCP客户端——服务端协议

news2025/1/15 20:01:02

目录

TCP客户端常见的API:

ServerSocket:

Socket:

TCP服务端(单线程版本)

      属性+构造方法:

      启动服务端的start()方法

        步骤一:接收客户端发送的socket

      步骤二: 调用processConnection方法来处理客户端发送的连接

         ①通过参数传入的clientSocket来获取输入、输出流对象,此处采用的是try()的方法来关闭流对象

  ②通过scanner.next()来不断读取内容;

 ③构造response,来作出响应

 ④通过OutputStream+PrintWriter来发送response字符串给客户端

⑤关闭连接

TCP客户端(单线程版本)

      属性+构造方法

     start方法

      步骤1:通过socket来获取到与服务端进行数据交互的inputStream和OutputStream

        步骤2:从控制台获取用户输入的信息

       步骤3:把读取到的request以流的形式发送给服务端,获取响应

       步骤4:通过Scanner读取服务器的响应,并且回显

 TCP服务端(支持多个客户端发送请求)

       多线程版本服务端 

      线程池版本服务端

TCP长连接/短连接问题

      短连接的工作过程:

     长连接的工作过程:


     TCP协议的具体介绍,已经在上一篇文章当中提到了。

     同时,上一篇文章也手写了一个Udp协议。

(2条消息) 认识UDP、TCP协议_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128709206?spm=1001.2014.3001.5501      在这一篇文章当中,udp的客户端和服务端之间的通信,使用的时DatagramSocket和DatagramPacket这两个api来完成传递信息的。DatagramSocke负责发送和接收消息,DatagramPacket用来传输报文


TCP客户端常见的API:

而在TCP协议当中,提供的API主要是下面两个类:

ServerSocket:专门给服务端使用的socket。

Socket:既可以提供给客户端使用,也可以给服务端使用。

ServerSocket:

构造方法:

方法签名方法说明
ServerSocket(int port)创建一个服务端嵌套字,并且指定服务端所占用的进程

成员方法: accept

方法签名方法说明
Socket accept()

accept方法,用来表示建立客户端与服务端的连接

前面一篇文章当中,我们提到了,TCP是"有连接"的协议,TCP客户端与服务端一定要建立连接,才可以互相发送消息。因此这个accept方法,返回的socket对象,服务端就是通过这个socket对象和客户端进行通信的。

如果服务端没有收到socket对象,那么就会阻塞等待,无法进行通信。


Socket:

       对于服务端来说,是由accept()方法返回的的,返回的socket对象用于和客户端进行通信。

 构造方法 

       对于客户端来说,在客户端的构造方法当中,需要构造对象的时候,指定一个IP以及端口号

这个IP以及端口号都是服务端


  两个常用普通方法

方法签名方法说明
getInputStream()通过socket对象,获取到内部的输入流对象
getOutputStream()通过socket对象,获取到内部的输出流对象

TCP服务端(单线程版本)

      属性+构造方法:

       需要在TcpEchoServer内部封装一个属性,这个属性是ServerSocket。

       在构造方法当中,需要指定ServerSocket占用哪个端口号,此端口号就是服务端的端口号

       代码实现:

/**
 * @author 25043
 */
public class TcpEchoServer {

    /**
     * 用于Tcp客户端与服务端通信
     * 的socket对象
     */
    private ServerSocket serverSocket;



    public TcpEchoServer(int port) throws IOException {
        //指定服务端进程占用的端口号
        serverSocket=new ServerSocket(port);
    }

      启动服务端的start()方法

        步骤一:接收客户端发送的socket

  //使用clientSocket来与客户端进行交流
  Socket clientSocket=serverSocket.accept();

        此处serverSocket.accept()方法的效果是接收客户端发送的连接。

        一个客户端对应一个accept方法获取的clientSocket

        由于Socket代表一个文件,任何一个文件会对应进程当中的一个文件描述符表。

        也就是这个socket会占用额外的磁盘空间,因此当客户端和服务端通信结束之后,需要把这个连接释放掉(释放的操作,会在后面提到)


        客户端在构造socket对象的时候,就会指定服务端的IP以及端口号。

        客户端如果想与服务端通信,一定需要建立连接!!因此,如果发服务端启动之后,没有客户端发送连接过来,那么服务端就会在accept()方法这里阻塞等待。


        因此,在服务端当中,通信的逻辑应当是这样的:

        


      步骤二: 调用processConnection方法来处理客户端发送的连接

       需要注意的是:一个Socket对应的是一个客户端发送的连接,但是在processConnection内部额有可能涉及处理多个客户端连接的步骤:也就是在Tcp协议当中,服务端客户端的关联关系为一对多。

       但是,以下的代码,先来体验一下单线程的模式。最后,将会演示一个多线程版本。

         ①通过参数传入的clientSocket来获取输入、输出流对象,此处采用的是try()的方法来关闭流对象

//获取clientSocket当中输入、输出流对象
try(InputStream inputStream= clientSocket.getInputStream();


OutputStream outputStream= clientSocket.getOutputStream()) {

  以下②③④一共3个步骤,需要在while(true)循环内部不断进行,直到scanner无法读取到内容了 

  ②通过scanner.next()来不断读取内容;

//2、根据请求构造响应


//通过scanner.next()的方式来读取,需要注意的是


//scanner遇到空格/换行符/其他空白字符会停止读取


//但是,读取的结果里面不会包含这三种符号


String request=scanner.next();

 ③构造response,来作出响应

//回写的内容
String response="服务端已经响应:"+request;

 ④通过OutputStream+PrintWriter来发送response字符串给客户端

  //使用PrintWriter来发送outputStream

  PrintWriter printWriter=new PrintWriter(outputStream);

  printWriter.println(response);

  //刷新缓冲区,保证当前数据一定会被发送出去

  printWriter.flush();

⑤关闭连接

 在finally代码块当中,关闭连接,释放文件描述符表。

finally {
           try {
                //关闭此次连接
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

  整体服务端代码(单线程版)

/**
 * @author 25043
 */
public class TcpEchoServer {

    /**
     * 用于Tcp客户端与服务端通信
     * 的socket对象
     */
    private ServerSocket serverSocket;



    public TcpEchoServer(int port) throws IOException {
        //指定服务端进程占用的端口号
        serverSocket=new ServerSocket(port);
    }

    /**
     * 启动服务端
     */
    public void start() throws IOException {
        System.out.println("启动服务端");
        while (true){
            //使用clientSocket来与客户端进行交流
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    /**
     * 处理客户端发送的连接
     * 客户端发送的连接@param clientSocket
     */
    private void processConnection(Socket clientSocket) {
        //输出客户端的IP以及端口号
        System.out.println("客户端已经上线!客户端的IP是:"
                +clientSocket.getInetAddress()+
                ";客户端的端口是:"+clientSocket.getPort());
        //获取clientSocket当中输入、输出流对象
        try(InputStream inputStream= clientSocket.getInputStream();
            OutputStream outputStream= clientSocket.getOutputStream()) {
            //使用while循环,处理多个请求+响应
            while (true){
                //1、通过scanner来读取inputStream
                Scanner scanner=new Scanner(inputStream);
                //读取完毕之后,直接返回:
                if(!scanner.hasNext()){
                    System.out.println("客户端已经下线!客户端的IP是:"
                            +clientSocket.getInetAddress()+
                            ";客户端的端口是:"+clientSocket.getPort());
                    //退出循环
                    break;
                }
                //2、根据请求构造响应
                //通过scanner.next()的方式来读取,需要注意的是
                //scanner遇到空格/换行符/其他空白字符会停止读取
                //但是,读取的结果里面不会包含这三种符号
                String request=scanner.next();
                //构造回写的内容response
                String response="服务端已经响应:"+request;
                //使用PrintWriter来发送outputStream
                PrintWriter printWriter=new PrintWriter(outputStream);
                printWriter.println(response);
                //刷新缓冲区,保证当前数据一定会被发送出去
                printWriter.flush();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

TCP客户端(单线程版本)

      属性+构造方法

        此处,需要一个socket,指定服务端的IP+端口号

 private Socket socket;

 public TcpEchoClient(String serverIp,int port) throws IOException {
    //指定服务端的ip+端口号
    socket=new Socket(serverIp,port);
 }

       如果客户端想和服务端进行通信,就一定需要指定服务端的端口号。因为TCP是有连接的协议,不允许在没有建立连接的情况下面发送消息。

       当socket对象被创建之后,也就意味着客户端成功与服务端建立连接。

       客户都安的socket创建之后的一瞬间,服务端的accept方法已经接收早到就客户端的socket对象。


     start方法

      步骤1:通过socket来获取到与服务端进行数据交互的inputStream和OutputStream

       需要注意的是,从客户端的socket获取的InputStream和OutputStream都是相对于客户端来进行输入/输出操作的。


        步骤2:从控制台获取用户输入的信息

   //1.客户端从键盘上面读取内容
   String request=input.next();

       步骤3:把读取到的request以流的形式发送给服务端,获取响应

                //2.把读取到的内容构造成请求,发送到客户端
                PrintWriter printWriter=new PrintWriter(outputStream);
                printWriter.println(request);
                //加上flush,刷新缓冲区
                printWriter.flush();

      可以看到,此处,使用的是printWriter.println(request)来发送字符串的

      但是,是否可以替换成print,也就是不采用\n呢?

      答案是,不可以:原因:

      在服务端当中,是使用Scanner scanner=input.next()来接收客户端发送的内容的:

     

       回顾一下scanner.next()在什么时候会停止读取,那就是在读取到\n或者空格或者空白字符的时候,就会停止读取。因此,此处客户端发送的内容当中,一定要带有\n,才可以确scanner.next()停止读取。 


步骤4:通过Scanner读取服务器的响应,并且回显

                //读取服务器响应
                Scanner scanner=new Scanner(inputStream);
                String response= scanner.next();
                //把响应的内容回显到界面上面
                System.out.println(response);

 整体客户端代码:

/**
 * Tcp客户端
 * @author 25043
 */
public class TcpEchoClient {
    private Socket socket;

    public TcpEchoClient(String serverIp,int port) throws IOException {
        //指定服务端的ip+端口号
        System.out.println("服务端已经指定端口号"+System.currentTimeMillis());
        socket=new Socket(serverIp,port);

    }
    public void start(){
        System.out.println("客户端启动!");
        Scanner input=new Scanner(System.in);
        //此处获取到的输入流、输出流对象,都是已经跟客户端建立了联系的
        try (InputStream inputStream= socket.getInputStream();
             OutputStream outputStream= socket.getOutputStream()){
            while (true){
                //1.客户端从键盘上面读取内容
                String request=input.next();
                //2.把读取到的内容构造成请求,发送到客户端
                PrintWriter printWriter=new PrintWriter(outputStream);


                printWriter.println(request);
                //加上flush
                printWriter.flush();
                //读取服务器响应
                Scanner scanner=new Scanner(inputStream);
                String response= scanner.next();
                //把响应的内容回显到界面上面
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        //指定服务端的ip以及端口号
        TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
        tcpEchoClient.start();
    }

}

单线程TCP存在问题分析:

      TCP的服务端的核心代码就是start方法

       当服务端启动之后,如果有客户端与服务端建立联系,那么accept方法就会返回一个socket对象,服务端使用这个socket对象与客户端进行通信。

       紧接着,服务端在processConnection方法当中,针对客户端发送过来的clientSocket进行不断地使用while循环,调用scanner.next方法进行读取操作。


       那么,也就意味着,只要客户端不下线,服务端就会一直停留在这个processConnection的while循环当中。

       由于在上述的代码当中,服务端的代码是单线程的。因此,服务端无法从processConnection方法当中离开,即使其他的客户端想再次给服务端建立连接,服务端也accept不到。


      但是,如果在processConnection当中不采用while循环,那么这样可以吗?

      也是不行的,原因:

         如果不使用while循环,那么,服务端只会读取一次客户端发送的请求,也就是调用一次scanner.next方法,然后服务端就会把连接给close掉了

        那么这个客户端如果想再次建立连接,就需要重新获取连接,也就是再次new一个Socket对象。

        但是,在上面客户端的代码当中,调用客户端构造方法的时候,只创建了一个连接,也就是一个socket。

         因此,如果客户端想要再次发送消息,就没有办法发送了。

           客户端代码: 

           


 TCP服务端(支持多个客户端发送请求)

       多线程版本服务端 

        大部分的代码写法都和单线程的一致,唯一的区别就在于,每调用一次processConnection方法需要创建新一个线程来执行。

        这样,每获取到一个clientSocket,就会创建一个新的线程t来执行processConnection方法。

        即使线程t出现了异常情况,无法结束运行,也不会影响主线程不断接收新的客户端连接。

        代码实现:

    /**
     * 启动服务端(多线程版)
     */
    public void start() throws IOException {
        System.out.println("启动服务端");
        while (true){
            //使用clientSocket来与客户端进行交流
            Socket clientSocket=serverSocket.accept();
            Thread t=new Thread(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
            t.start();
        }
    }

      线程池版本服务端

       以上代码,在客户端数量不大的情况下面,是可以行得通的

       但是,如果客户端数量比较庞大,并且线程的创建销毁工作也是开销比较大的,因此,可以考虑使用线程池来处理processConnection方法,这样就可以减少了线程不断创建、销毁带来的开销。

        代码实现:

 private ExecutorService threadPool= Executors.newCachedThreadPool();
    /**
     * 启动服务端(多线程版)
     */
    public void start() throws IOException {
        System.out.println("启动服务端");
        while (true){
            //使用clientSocket来与客户端进行交流
            Socket clientSocket=serverSocket.accept();
            //往线程池当中提交任务
            threadPool.submit(() -> processConnection(clientSocket));
        }
    }

TCP长连接/短连接问题

      短连接的工作过程:

       客户端与服务器建立连接

       发送一次请求

       读取响应

       关闭连接

       下次通信,就需要再一次建立连接。可以看到,短连接每一次通信只会建立一次连接


     长连接的工作过程:

        ①客户端与服务端建立连接

        ②客户端发送消息

        ③读取响应

        ④根据需求,尝试再次发送消息(也就是回到2)

        ⑤重复之间若干次,再决定是否断开连接


       可以看到,长连接的特点就是一次连接多次发送消息。而短连接,就是一次连接只可以发送一次请求。看似长连接的复用性更高,但是其实也不一定说要使用长连接的策略才好,需要结合具体的应用场景。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/178630.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

影像组学|特征定义以及提取

一、 影像组学特征分类 1.1 影像组学特征分类 1.1.1 一阶统计特征 一阶统计特征,反应所测体素的对称性、均匀性以及局部强度分布变化。包括中值,平均值,最小值,最大值,标准差,偏度,峰度等。 …

【Linux】六、Linux 基础IO(三)|文件系统|软硬链接|文件的三个时间

目录 八、文件系统 8.1 磁盘 8.1.1 磁盘的物理结构 8.1.2 磁盘的存储结构 8.1.3 磁盘的逻辑结构 8.2 inode 九、软硬链接 9.1 软链接 9.2 硬链接 9.3 当前路径(.)和上级路径(..) 十、文件的三个时间 八、文件系统 上面的内容谈论的都是一个被打开文件,那…

如何将两个录音合成一个?这篇文章告诉你

现如今,很多小伙伴都加入到短视频行业当中。而短视频的制作往往需要将多段音频进行一个合并。那么问题来了,当你想多个音频进行合并在一起的时候,你是怎么做的呢?其实很简单,我们只需要借助市面上的一些合并软件就好了…

初始网络

文章目录初始网络局域网 / 广域网IP地址 和 端口号认识协议协议分层初始网络 这里可以先自行在网上了解一下网络的发展史 也就是互联网是怎么来的. 局域网 / 广域网 关于网络的发展史 , 会涉及到两个非常重要的术语 ,也就是 局域网,和广域网 。 局域网 &…

JavaEE多线程-阻塞队列

目录一、认识阻塞队列1.1 什么是阻塞队列?1.2 生产者消费者模型1.3 标准库中的阻塞队列类二、循环队列实现简单阻塞队列2.1 实现循环队列2.2 阻塞队列实现一、认识阻塞队列 1.1 什么是阻塞队列? 阻塞队列:从名字可以看出,他也是…

简明Java讲义 2:数据类型和运算符

目录 1、安装IDE编辑器 2、关键字和保留字 3、标识符 4、分隔符 5、数据类型 6、基本类型的数据类型转换 7、表达式类型的自动提升 8、变量 9、运算符 10、运算符的优先级 1、安装IDE编辑器 在开始内容之前,先下载IDE,可以是Eclipse或STS&…

Python函数(函数定义、函数调用)用法详解

Python 中函数的应用非常广泛,前面章节中我们已经接触过多个函数,比如 input() 、print()、range()、len() 函数等等,这些都是 Python 的内置函数,可以直接使用。除了可以直接使用的内置函数外,Python 还支持自定义函数…

LeetCode刷题模版:201 - 210

目录 简介201. 数字范围按位与202. 快乐数203. 移除链表元素204. 计数质数205. 同构字符串206. 反转链表207. 课程表【未实现】208. 实现 Trie (前缀树)209. 长度最小的子数组210. 课程表 II【未实现】结语简介 Hello! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您…

LeetCode[1319]连通网络的操作次数

难度:中等题目:用以太网线缆将 n台计算机连接成一个网络,计算机的编号从 0到 n-1。线缆用 connections表示,其中 connections[i] [a, b]连接了计算机 a和 b。网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其…

(十六)异步编程

CompletableFuture在Java8中推出,Java8中的异步编程就是依靠此类。几种任务接口四种任务无参数有一个参数有两个参数无返回值RunnableConsumerBiConsumer有返回值SupplierFunctionBiFunctionCompletionStage接口这个类中定义的许多能够链式调用的方法和组合方法时是…

Unity3DVR开发—— XRInteractionToolkit(PicoNeo3)

目录 一、开发前的准备 二、基础配置 三、Pico项目配置 四、添加基础功能 一、开发前的准备 1、为了方便开发,先在Pico开发者平台里下载预览工具 Pico开发者平台https://developer-global.pico-interactive.com/sdk?deviceId1&platformId1&itemId17 2、…

【哈希表】关于哈希表,你该了解这些!

【哈希表】理论基础1 哈希表2 哈希函数3 哈希碰撞3.1 拉链法3.2 线性探测法4 常见的三种哈希结构5 总结1 哈希表 哈希表 Hash table (一些书籍翻译为散列表) 哈希表是根据关键码的值而直接进行访问的数据结构。 直白来讲其实数组就是一张哈希表。 哈希表…

用1行Python代码识别增值税发票,YYDS

大家好,这里是程序员晚枫。 录入发票是一件繁琐的工作,如果可以自动识别并且录入系统,那可真是太好了。 今天我们就来学习一下,如何自动识别增值税发票并且录入系统~ 识别发票 识别发票的代码最简单,只需要1行代码…

CSS的总结

从HTML被发明开始,样式就以各种形式存在。不同的浏览器结合它们各自的样式语言为用户提供页面效果的控制。最初的HTML只包含很少的显示属性。 随着HTML的成长,为了满足页面设计者的要求,HTML添加了很多显示功能。但是随着这些功能的增加&…

ISIS的路由器级别level-1、level-2、level-1-2,报文格式

2.1.0 ISIS的路由器级别level-1、level-2、level-1-2,报文格式 通过该文章了解ISIS的路由器级别类型、级别之间建立的邻接关系、各级别的作用、ISIS报文的结构。 ISIS路由器级别 Level-1 level-1路由器又称L1路由器,是一种ISIS区域内部路由&#xff0c…

6、运算符

目录 一、赋值运算符 二、算数运算符 三、自增和自减运算符 四、比较运算符 五、逻辑运算符 六、位运算符 1. “按位与”运算 2. “按位或”运算 3. “按位取反”运算 4. “按位异或”运算 5. 移位操作 七、三元运算符 八、运算符优先级 一、赋值运算符 赋值运算…

[Android开发基础1] 五大常用界面布局

文章目录 一、线性布局 二、相对布局 三、帧布局 四、表格布局 五、约束布局 总结 一、线性布局 线性布局(LinearLayout)主要以水平或垂直方式来显示界面中的控件。当控件水平排列时,显示顺序依次为从左到右,当控件垂直排列时…

29/365 java 网络通信 IP InetAddress

1.网络通信: 如何定位到一台主机? IP地址 定位主机, 端口号定位到具体的应用程序 如何在主机之间通信(传输数据)? 网络通信协议 2.IP地址分类 IPv4: 32位 IPv6地址:128位 IPv6地址使用以冒号…

初学python100例-案例37 合并排序列表 少儿编程python编程实例讲解

目录 python合并排序列表 一、题目要求 1、编程实现 2、输入输出

C语言形参和实参的区别

如果把函数比喻成一台机器,那么参数就是原材料,返回值就是最终产品;从一定程度上讲,函数的作用就是根据不同的参数产生不同的返回值。这一节我们先来讲解C语言函数的参数,下一节再讲解C语言函数的返回值。C语言函数的参…