javaEE 初阶 — TCP 流套接字编程

news2024/11/17 23:29:57

文章目录

  • 1. TCP 流套接字
    • 1.1 ServerSocket API
    • 1.2 Socket API
    • 1.3 TCP中的长短连接
  • 2. TCP 版本的回显服务器
  • 3. TCP 版本的回显客户端
  • 4. 如何给多个客户端提供服务

1. TCP 流套接字


TCP 不需要一个类;来表示 “TCP” 数据报。
TCP 不是以数据报为单位进行传输的,而是以字节流的方式进行传输的。

1.1 ServerSocket API


ServerSocket 是专门给服务器使用的 Socket 对象。

ServerSocket 构造方法:

  • ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket 方法:

1.2 Socket API


Socket 是既会给客户端使用,也会给服务器使用。

不管是客户端还是服务端 Socket,都是双方建立连接以后,保存的对端信息,即用来与对方收发数据的。


Socket 构造方法:



Socket 方法:

  • InetAddress getInetAddress()返回套接字所连接的地址
  • InputStream getInputStream()返回此套接字的输入流
  • OutputStream getOutputStream()返回此套接字的输出流

进一步通过 socket 对象,获取到内部的 流对象,借助流对象来进行发送/接收。


在服务器这边,是由 accept 返回的。
在客户端这边,是由代码构造的。构造的时候指定一个 ip 和 端口号。(此处指定的是服务器的 ip 和 端口)

1.3 TCP中的长短连接


TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

短连接: 每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据

长连接: 不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据

区别:

  • 建立与关闭的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要
    第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时
    的,长连接效率更高。
  • 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送
    请求,也可以是服务端主动发。
  • 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于
    客户端与服务端通信频繁的场景,如聊天室,实时游戏等。

2. TCP 版本的回显服务器


1、先用 ServerSocket 创建一个对象。

  private ServerSocket serverSocket = null;



指定一个构造方法来绑定一个端口

public TcpEchoServer(int port) throws IOException {
    serverSocket = new ServerSocket(port);
}



创建一个 start 方法,通过创建的对象来调用 accept 方法(accept 是接收连接)

public void start() throws IOException{
    System.out.println("启动服务器!!!");
    while (true) {
        //使用 clientSocket 和具体的客户端进行交互
        Socket clientSocket = serverSocket.accept();
    }
}

accept 方法的效果是 “接收连接”,前提是要有客户端来建立连接。

客户端在构造 Socket 对象的时候,就会指定服务器的 IP 和 端口号,如果没有客户端来连接,此时 accept 就会阻塞


accept 会返回一个 Socket 对象

Socket clientSocket = serverSocket.accept();


TCP socket 里面涉及到两种 socket 对象

此时的 clientSocket 是一个 socket 对象,serverSocket 也是一个 socket 对象。

下面来举个例子说明。

比如张三去 4s 店买车,刚进店的时候,店员小王就来接待他,在了解了需求后就把张三汽车介绍的员工那里。
这个时候小王就把张三带到李四那里,而李四是专门为顾客介绍汽车有哪些特点等等的。

店员小王负责的是具体的买车服务,而李四负责的是引领顾客到指定位置。
店员小王就相当于是 serverSocket 对象,讲解员李四 就相当于是 clientSocket 对象。


2、接收到连接后,就可以写一个方法拿 clientSocket 与客户端进行交互了。

使用这个方法来处理一个链接,这一个连接对应到一个客户端,但是这里可能会涉及到多次交互。

private void processConnection(Socket cilentSocket) {
    System.out.printf("[%s:%d] 客户端上线!\n", cilentSocket.getInetAddress().toString(), cilentSocket.getPort());
 
}


cilentSocket.getInetAddress() 是获取 ip 地址 ,cilentSocket.getPort() 是获取端口号。


接下来就可以基于上述 socket 对象和客户端进行通信了。

先用 try catch 拿到 InputStreamOutputStream

    try (InputStream inputStream = cilentSocket.getInputStream();
         OutputStream outputStream = cilentSocket.getOutputStream()){
     
    } catch (IOException e) {
        e.printStackTrace();
    }


接下来读取请求

Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()) {
   // 没有下个数据说明读完了(也就是客户端关闭连接)
   System.out.printf("[%s:%d] 客户端下线!\n", cilentSocket.getInetAddress().toString(),
           cilentSocket.getPort());
   break;
}
// 
String request = scanner.next();


注意此处使用的 next 是一直读取到换行符/空格/其他空白符结束,但是最终返回结果里不包含上述空白符。


根据请求构造响应

调用 process 来构造请求,response 来接收返回的 request。

 String response = process(request);
 public String process(String request) {
     return request;
 }


返回响应结果

OutputStream 没有 write 字符串这样的功能,可以把 字符串里的字节数组拿出来进行写入,也可以用字符流转换一下。

PrintWriter printWriter = new PrintWriter(outputStream);
// 此时使用 println 来进行写入,让结果中带有一个 \n 换行,方便对端来进行接收解析
printWriter.println(response);


要使用 flush 用来刷新缓冲区,保证当前写入的数据确实是发送出去了。

 printWriter.flush();


打印一下日志

System.out.printf("[%s:%d] rep: %s; resp: %s \n", cilentSocket.getInetAddress().toString(),
        cilentSocket.getPort(), request, response);


这个代码中用到了一个 cilentSocket ,此时任意一个客户端连上来,都会返回/创建一个 Socket 对象。( Socket 就是文件)

 Socket clientSocket = serverSocket.accept();
 processConnection(clientSocket);

每次创建一个 cilentSocket 对象,就要占用一个文件描述符表的位置。
因此在使用完毕之后,就需要进行释放。

前面的 socket 都没有释放,一方面这些 socket 生命周期更长(跟随这个程序),另一方面这些 socket 也不多,数量是固定的。
但是此处的 cilentSocket 数量多,每个客户端都有一个,生命周期也更短。


调用 close 来关闭,并且将 close 放到 finally 里面,保证一定能执行到。

finally {
  try {
      // 将 close 放到 finally 里面,保证一定能执行到
      cilentSocket.close();
  }catch (IOException e) {
      e.printStackTrace();
  }
}

完整代码

package network;

import java.io.*;
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{
        System.out.println("启动服务器!!!");
        while (true) {
            //使用 clientSocket 和具体的客户端进行交互
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    // 使用这个方法来处理一个链接
    // 这一个连接对应到一个客户端,但是这里可能会涉及到多次交互
    private void processConnection(Socket cilentSocket) {
        System.out.printf("[%s:%d] 客户端上线!\n", cilentSocket.getInetAddress().toString(), cilentSocket.getPort());
        // 基于上述 socket 对象和客户端进行通信
        try (InputStream inputStream = cilentSocket.getInputStream();
             OutputStream outputStream = cilentSocket.getOutputStream()){
            // 由于要处理多个请求和响应,也是使用循环来进行
            while (true) {
                // 1.读取请求
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    // 没有下个数据说明读完了(也就是客户端关闭连接)
                    System.out.printf("[%s:%d] 客户端下线!\n", cilentSocket.getInetAddress().toString(),
                            cilentSocket.getPort());
                    break;
                }
                // 注意此处使用的 next 是一直读取到换行符/空格/其他空白符结束,但是最终返回结果里不包含上述空白符
                String request = scanner.next();
                // 2.根据请求构造响应
                String response = process(request);
                // 3.返回响应结果
                // OutputStream 没有 write String 这样的功能,可以把 String 里的字节数组拿出来进行写入,也可以用字符流转换一下
                PrintWriter printWriter = new PrintWriter(outputStream);
                // 此时使用 println 来进行写入,让结果中带有一个 \n 换行,方便对端来进行接收解析
                printWriter.println(response);
                // flush 用来刷新缓冲区,保证当前写入的数据确实是发送出去了
                printWriter.flush();
                System.out.printf("[%s:%d] rep: %s; resp: %s \n", cilentSocket.getInetAddress().toString(),
                        cilentSocket.getPort(), request, response);
            }
            cilentSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // 将 close 放到 finally 里面,保证一定能执行到
                cilentSocket.close();
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException{
        TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
        tcpEchoServer.start(); //启动
    }
}

3. TCP 版本的回显客户端


创建一个 socket 对象,客户端使用 Socket 来创建。

private Socket socket = null;



这个 socket 对象 和 服务器的 clientSocket 对象不是同一个对象,相当于是电话的两端。


指定一个构造方法来绑定 ip 和 端口

public TcpEchoClient(String serverIp, int serverPort) throws IOException {
    // Socket 构造方法,能够识别点分十进制式的 ip 地址,比 DatagramPacket 更方便
    // new 这个对象的同时,就会进行 TCP 连接操作
    socket = new Socket(serverIp, serverPort);
}

这里绑定的是服务器的 ip 和 端口。


写一个 start 方法来启动客户端

1、先从键盘上获取用户输入的内容

Scanner scanner = new Scanner(System.in);
String request = scanner.next();
if (request.equals("exit")) {
   System.out.println("goodbye");
   break;
}


2、把读到的内容构造成请求,发送给服务器

因为 socket 里面包含了一个输入流和一个输出流,借助 输出流来发送,借助输入流来接收
所以要把这两个流对象准备好,在 while 循环的外面套上一层 try catch

try (InputStream inputStream = socket.getInputStream();
     OutputStream outputStream = socket.getOutputStream()){
    while (true) {
               
    }
} catch (IOException e) {
    e.printStackTrace();
}


由于 OutputStream 没有写字符串这样的功能,可以拿一个 PrintWriter 类来把它包装一下。

 PrintWriter printWriter = new PrintWriter(outputStream);
 printWriter.println(request); //直接打印


刷新缓冲区,保证数据确实发送出去了。

 printWriter.flush(); 



3、使用 inputStream 读取服务器的响应

Scanner respScanner = new Scanner(inputStream);
String reaponse = respScanner.next();



4、把响应内容显示到界面上

System.out.println(reaponse);

客户端 和 服务器 用到的 API 差不多,只是执行的流程不太一样。
服务器 是先接收再发送,客户端 是先发送再接收。

完整代码

package network;

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 构造方法,能够识别点分十进制式的 ip 地址,比 DatagramPacket 更方便
        // new 这个对象的同时,就会进行 TCP 连接操作
        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()){
            while (true) {
                System.out.print("> ");
                // 1.先从键盘上获取用户输入的内容
                String request = scanner.next();
                if (request.equals("exit")) {
                    System.out.println("goodbye");
                    break;
                }

                // 2.把读到的内容构造成请求,发送给服务器
                // OutputStream 没有写字符串这样的功能,可以拿一个 PrintWriter 类来把它包装一下
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request); //直接打印
                printWriter.flush(); // 刷新缓冲区,保证数据确实发送出去了

                // 3.读取服务器的响应
                Scanner respScanner = new Scanner(inputStream);
                String reaponse = respScanner.next();

                // 4.把响应内容显示到界面上
                System.out.println(reaponse);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException{
        TcpEchoClient tcpEchoClient = new TcpEchoClient("127.0.0.1", 9090);
        tcpEchoClient.start();
    }
}


运行结果

先运行服务器



再运行客户端。

 printWriter.println(response);
 printWriter.println(request); 


当前的代码里面使用的是 println 来进行发送数据的,而 println 会在发送的数据后面自动加上 \n 换行。
如果不使用 prinln 而是使用 print (不带换行的),这个代码是否可以正常运行???


当前的代码。没有 \n 肯定是不行的!!!

TCP 协议是面向字节流的协议,(字节流的特性:一次读多少个字节随意)但是接收方如何知道这一次读了多少个字节呢???

这就需要在传输数据的时候进行明确的约定,此处的代码中隐式约定了使用 \n 来作为当前的请求/响应分割约定。

\

4. 如何给多个客户端提供服务


当前的服务器,同一时刻只能给一个客户端提供服务,显然这是不科学的。


当前启动服务器后,先启动 客户端1,可以看到正常的上线提醒。






如果再启动 客户端2 就看不到任何提示了。




可以看到不仅没有提示 客户端2 上线,而且 客户端2 发送消息后也没有任何提示。


当把 客户端1 退出后就一切正常了。





而且此时的 客户端2 可以发送消息了。






当有客户端脸上服务器之后,代码就执行到了这个 processConnection 方法里的循环中了。
此时意味着,只要这个循环不结束,processConnection 方法就结束不了,进一步也就无法第二次调用到 accept方法。

也就不能给多个线程提供服务。

解决办法就是:使用多线程
主线程负责进行 accept,每次收到一个连接,就创建新线程,有这个新线程负责处理这个新的客户端。

每个线程都是独立的执行流,每个独立的执行流都是各自执行各自的逻辑,彼此之间是并发的关系。
不会说这边阻塞,会影响到另一边的执行。


创建一个线程,在这个线程里面调用 processConnection 方法。

// 创建一个线程
Thread thread = new Thread(() ->{
    processConnection(clientSocket);
});


如果此时的客户端比较多,创建和销毁线程就会比较频繁,此时建议使用线程池

先创建一个 线程池,再将 processConnection 方法的调用放到线程池中。

// 此处使用 newCachedThreadPool 而不使用 FixedThreadPool,因为创建的线程数量不太应该是是固定的。
ExecutorService threadPool = Executors.newCachedThreadPool();
// 线程池版本
threadPool.submit(() -> {
    processConnection(clientSocket);
});




根据代码的结果就可以看到此时就可以支持服务多个客户端了。

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

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

相关文章

拉伯配资“十年一剑”硕果累累 我国注册制改革迈入新征程

从2013年党的十八届三中全会明确提出“推动股票发行注册制变革”,到首届进博会上宣告科创板试点注册制,再到本年2月1日全面施行股票发行注册制变革正式发动,十年风雨兼程,我国注册制逐渐从“试点”走向“全面”。 2013年11月&…

编译链接过程详解

写在前面: 大家都知道,我们在编译器中建好一个**.c或.cpp 文件**,经过编译之后就可以运行了,也就是说我们写的.c 文件最后会变成一个可执行程序,那么 .c 或者 .cpp 文件是如何变成一个可执行程序的呢? 主要…

Vue计算属性和监视属性

目录 计算属性computed 监事属性 深度监视 计算属性computed 计算属性: 定义:要用的属性不存在,要通过已有属性计算得来 2、原理:底层借助了Object.defineproperty方法提供的getter和setter 3、get函数什么时候执行? 1、初…

移动硬盘修复的有效方法,恢复移动硬盘的数据这么做!

硬盘是计算机中的存储设备,是非常重要的部分。当硬盘发生故障,很可能会导致我们电脑里面的数据丢失。所以移动硬盘发生故障,我们一定要想办法修复它。 有没有什么操作方法,我们自己也可以简单进行?移动硬盘修复其实也…

mariadb数据库删除恢复过程

不作不死,不小心使sqlyog导数据选错服务器。把生产机的数据全部删除了。可怕的数据没有做其他过多的备份,只是每天自动crontab 备份。该怎么办呢?头脑一片空白。快, 赶紧看看日备份有没有。马上切换到备份目录,喜出望外…

构建指标体系是一套数据分析的框架,比如看哪些指标,这些指标变化了就会反映什么问题,是这样的吗?

指标体系是指由若干个反映企业业务运营特征的相对独立又相互联系的统计指标所组成的有机整体。近年来,各类企业逐渐认识到业务指标的重要性,从管理者们长期关注的企业绩效考核,到用来体现信息化水平的数据可视化大屏,其背后都离不…

【IoT】寻光智能车与循迹智能车

1、寻光智能车 功能说明 智能寻光小车,智能识别光线强弱,实现小车永远向光最强的地方行走,到光源处小车自动停止。基本车体为三轮、二驱、双层机构。主控芯片采用最常用的51单片机; 驱动采用L9110驱动芯片驱动两个减速直流电机…

Veeam ONE v12 发布 (含下载) - 面向所有工作负载的 IT 监控解决方案

Veeam Availability Suite v12 请访问原文链接:https://sysin.org/blog/veeam-one-12/,查看最新版。原创作品,转载请保留出处。 作者主页:www.sysin.org 概述 保持全面可视性和控制力,以高效执行管理、优化、计划和…

域渗透漏洞

一、域内提权漏洞 (CVE-2021-42287和CVE-2021-42278) 1.1 漏洞介绍 1.1.1 CVE-2021-42278 主机账户的名称尾部应该有一个 $(即 sAMAccountName 属性),但是域控对此属性并没有任何验证来确保是否带有 $,这允许攻击者模拟域控主机账户。 1.1.2 CVE-2021-42…

python带你制作自动答题程序,速度超越98%人

前言 嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! 目录前言环境使用:模块使用:自动答题思路步骤:代码展示尾语 &#x1f49d;环境使用: Python 3.8 –> 解释器 <执行python代码> Pycharm –> 编辑器 <写python代码的> 模块使用: import requests —&g…

Springboot_vue摄影作品图片展示交流平台

前后端通讯一般都是采取标准的JSON格式来交互。 前后端分离 的核心思想是前端页面通过 ajax 调用后端的 restuful api 进行数据交互&#xff0c;而 单页面应用&#xff08;single page web application&#xff0c;SPA&#xff09;&#xff0c;就是只有一张页面&#xff0c;并在…

https解决方案-利用keytool生成证书

https解决方案 1:什么是HTTPS&#xff1f; HTTPS其实是有两部分组成&#xff1a;HTTP SSL / TLS&#xff0c; 也就是在HTTP上又加了一层处理加密信息的模块&#xff0c;并且会进行身份的验证。 2:什么是自签名证书&#xff1f; 就是自己生成的证书&#xff0c;并不是官方…

计算机中的大小端存储

在讲C语言的时候&#xff0c;有讲到大小端的内容&#xff0c;这里呢对大小端的相关内容进行了整理&#xff0c;有需要的可以参考一下&#xff01; 大端和小端来自一个小故事&#xff1a;端模式(Endian)的这个词出自Jonathan Swift书写的《格列佛游记》。这本书根据将鸡蛋敲开的…

mysql导致索引失效的常见情况以及命名规范索引计划分析

1、失效的情况 1.前导模糊查询不能利用索引(like ‘%XX’或者like ‘%XX%’) 假如有这样一列code的值为’AAA’,‘AAB’,‘BAA’,‘BAB’ ,如果where code like %AB’条件&#xff0c;由于前面是 模糊的&#xff0c;所以不能利用索引的顺序&#xff0c;必须一个个去找&#xf…

虚拟机的Linux安装redis

1&#xff0c;下载redis 可以去官网下载压缩包 2&#xff0c;环境准备 我看很多文章都说要安装yum&#xff0c;但是在终端界面一直都是安装失败&#xff0c;后面才知道我的虚拟机是Ubuntu版本&#xff0c;而Ubuntu版本一般不使用yum命令&#xff0c;一般都是使用apt-get命令…

RANSAC: Random Sample Consensus

目录RANSAC算法基本思想和流程迭代次数推导参考RANSAC RANSAC(RAndom SAmple Consensus,随机采样一致)算法是从一组含有外点(outliers)的数据中正确估计数学模型参数的迭代算法。“外点”一般指的的数据中的噪声&#xff0c;比如说匹配中的误匹配和估计曲线中的离群点。所以&a…

电脑黑屏却开着机是怎么回事?解决黑屏的快捷方法

我们经常会用到电脑&#xff0c;但是你真的了解电脑相关知识吗&#xff1f;遇到比较复杂的电脑问题&#xff0c;你是否会手足无措&#xff1f;电脑黑屏却开着机这是什么原因造成的&#xff0c;我们有什么好办法恢复黑屏的电脑吗&#xff1f; 本文针对电脑黑屏却开着机的情况&a…

PCB生产工艺 | 第三道之沉铜,你都了解吗?

衔接上文&#xff0c;继续为朋友们分享普通单双面板的生产工艺流程。 如图&#xff0c;第三道主流程为沉铜。 沉铜的目的为&#xff1a; 在整个印制板&#xff08;尤其是孔壁&#xff09;上沉积一层薄铜&#xff0c;以便随后进行孔内电镀&#xff0c;使孔金属化&#xff08;孔…

小众点评项目要点内容总结【面试用】

小众点评项目要点 文章目录小众点评项目要点1.使用Redis代替Session登录1.1 Session登录存在的问题1.2 使用Redis代替Session登录分析1.3 使用Redis登录的流程1.4 解决Redis中有效期问题2.使用Redis作为缓存2.1 为什么使用缓存2.2 缓存策略2.3 解决缓存穿透2.4 解决缓存雪崩2.5…

CSS响应式设计——(视口/网格视图/媒体查询/图像/视频)看这一篇就够了

目录 响应式网页设计 - 简介 什么是响应式网页设计&#xff1f; 为所有用户获得最佳体验的设计 响应式网页设计 - 视口 什么是视口&#xff1f; 设置视口 把内容调整到视口的大小 响应式网页设计 - 网格视图 什么是网格视图&#xff1f; 构建响应式网格视图 实例 C…