(Java高级教程)第三章Java网络编程-第四节:TCP流套接字(ServerSocket)编程

news2024/12/25 1:14:33

文章目录

  • 一:Java流套接字通信模型
  • 二:相关API详解
    • (1)ServerSocket
    • (2)Socket
  • 三:TCP通信示例一:客户端发送什么服务端就返回什么
    • (1)代码
    • (2)效果展示
    • (3)分析
  • 四:TCP通信示例二:多线程版本
    • (1)单线程版本存在的问题
    • (2)代码
    • (3)效果展示
  • 五:TCP通信示例三:线程池版本
    • (1)多线程版本存在的问题
    • (2)代码
    • (3)效果展示

一:Java流套接字通信模型

Java TCP通信模型:Java中使用TCP协议进行通信,主要依靠以下两个类

  • ServerSocket:是创建TCP服务端Socket的API
  • Socket API:是客户端Socket,或服务端中接收到客户端连接(accept方法)的请求后,返回服务端Socket

通信流程如下
在这里插入图片描述

二:相关API详解

(1)ServerSocket

ServerSocket:用于创建TCP服务端流套接字Socket

构造方法如下

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

成员方法如下

方法签名方法说明
Socket accept()开始监听端口,当有客户端连接后会返回一个服务端Socket对象,并基于该Socket 与客户端建立连接,否则阻塞等待
void close()关闭此套接字

(2)Socket

Socket :是客户端的Socket(当然也会给服务端用,上面表格说过,当有客户端连接服务端后,会返回一个服务端Socket

构造方法如下

方法签名方法说明
Socket(String host, int port)创建一个客户端流套接字Socket,并和对应IP的主机上、对应端口的进程建立连接

成员方法如下

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

三:TCP通信示例一:客户端发送什么服务端就返回什么

  • 注意:这个功能比较简单,但主要目的是为了演示上面所讲API的用法

(1)代码

服务端IP地址设置为127.0.0.1,也即本地环回,也即自己发自己收,数据会完整走一遍协议

服务端TCPServer:

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;

public class TCPServer {
    // 创建监听套接字
    private ServerSocket listenSocket = null;

    public TCPServer(int port) throws IOException {
        // 监听套接字绑定指定端口
        listenSocket = new ServerSocket(port);
    }

    // 服务器启动
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 调用监听套接字的accept()连接客户端,并返回Socket类型的clientSocket
            // 将clientSocket传递给具体处理连接的方法processConnection()进行处理
            Socket clientSocket = listenSocket.accept();
            // 进行处理
            processConnection(clientSocket);
        }
    }
    // 用于处理连接
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.println("【客户端IP: " + clientSocket.getInetAddress().toString()
                + "客户端口号:" + clientSocket.getPort() + "】"
                + "已上线");

        // 处理请求
        // 打开inputStream和outputStream
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            while (true) {
                // 1. 读取请求并解析
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    // 如果读完了那么连接可以断开了
                    System.out.println("【客户端IP: " + clientSocket.getInetAddress().toString()
                            + "客户端口号:" + clientSocket.getPort() + "】"
                            + "下线");
                    break;
                }
                String request = scanner.next();
                // 2. 根据请求计算响应,具体处理函数为process
                String response = process(request);

                // 3. 响应回复给客户端
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                printWriter.flush();

                // 打印信息
                System.out.println("【客户端IP: " + clientSocket.getInetAddress().toString()
                        + "客户端口号:" + clientSocket.getPort() + "】"
                        + ":\"" + request + "\"" + ", 服务端回复: " + "\"" + response + "\"");
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭套接字
            // listenSocket在TCP服务端程序中只有一个,所以不太可能把文件描述符占满
            // 而clientSocket 每遇到一个客户端都要创建一个,所以一定要注意关闭
            clientSocket.close();
        }
     }

     // 业务逻辑函数
    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TCPServer server = new TCPServer(9090);
        server.start();
    }
}

客户端TPCClient:

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 TCPClient {
    // 建立Socket对象
    private Socket socket = null;

    public TCPClient(String serverIP, int serverPort) throws IOException {
        // 指定服务端IP和端口号
        socket = new Socket(serverIP, serverPort);
    }

    // 客户端启动
    public void start () throws IOException {
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                // 1. 获取用户输入
                System.out.print("input: ");
                String request = scanner.next();
                // 2. 发送请求给服务端
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                printWriter.flush();
                // 3. 从服务端获得响应
                Scanner responseScanner = new Scanner(inputStream);
                String response = responseScanner.next();

                // 打印信息
                System.out.println("服务端回复:" + response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

}

(2)效果展示

在这里插入图片描述

(3)分析

对于服务端(TCPServer类)

  • 构造方法(public TCPServer(int port)

    • 需要建立一个ServerSocket类型的监听套接字,用于监听客户端的请求连接,也即private ServerSocket listenSocket = new ServerSocket(port)
  • 服务端处理逻辑(public void start()

    • 不断循环一直监听客户端的连接,当有客户端连接之后监听套接字会返回Socket类型的套接字用于处理这个连接,也即Socket clientSocket = listenSocket.accept()
    • 具体处理连接的过程交由processConnection()方法进行,也即processConnection(ClientSocket)
  • 服务端处理连接(private void processConnection(Socket clientSocket)

    • ①:打开套接字的输入流和输出流

      • clientSocket里的请求内容保存在其InputStream中,最终服务端回复响应时要将该响应写入到其OutputStream中,也即InputStream inputStream = clientSocket.getInputStream()OutputStream outputStream = clientSocket.getOutputStream()
    • ②:读取InputStream中的请求并解析

      • 使用Scanner进行读取比较方便,也即Scanner scanner = new Scanner(inputStream)
      • 读取时注意随时判断是否读取完毕,如果读取完毕表示客户端可以下线了
      • 读取好的请求保存在request中,也即String request = scanner.next()
    • ③:根据请求得到响应

      • request后,需要对该request进行处理(交给方法process),不同的业务逻辑会有不同的处理方法。这里我们只是简单的“回显”一下即可,也即客户端发什么服务端就回复什么
    • ④:将响应写入到OutputStream

      • 使用PrintWriter 写入比较方便,也即PrintWriter printWriter = new PrintWriter(outputStream)printWriter.println(response)
      • 写入完成之后必要忘记刷新一下,也即printWriter.flush()
    • ⑤:打印相关信息

    • ⑥:关闭clientSocket套接字

      • listenSocket在TCP服务端程序中只有一个,所以不太可能把文件描述符占满,而clientSocket 每遇到一个客户端都要创建一个,所以一定要注意关闭,也即 clientSocket.close()
  • main方法

    • 构造TCPServer对象,并绑定指定端口号,如9090,也即TCPServer server = new TCPServer(9090)
    • 启动服务端,也即server.start()

对于服务端(TCPClient类)

  • 构造方法(public TCPClient(String serverIP, int serverPort)

    • 需要建立一个Socket类型的套接字,并传入服务端IPPort,也即private Socket socket = new Socket(serverIP, serverPort)
  • 客户端处理逻辑(public void start ()

    • ①:打开套接字的输入流和输出流

      • 客户端会把它的请求写入到InputStream中,服务端回复响应后客户端会从 OutputStream 中读取,也即InputStream inputStream = socket.getInputStream()OutputStream outputStream = socket.getOutputStream()
    • ②:读取客户端用户输入并构造请求

      • 使用Scanner接受即可,也即String request = scanner.next()
    • ③:将请求写入到OutputStream

      • 使用PrintWriter 写入比较方便,也即PrintWriter printWriter = new PrintWriter(outputStream)printWriter.println(request)
      • 写入完成之后必要忘记刷新一下,也即printWriter.flush()
    • ④:读取InputStream中的响应

      • 使用Scanner进行读取比较方便,也即Scanner responseScanner = new Scanner(inputStream)String response = responseScanner.next()
    • ⑤:打印相关信息

  • main方法

    • 构造TCPClient对象,并给定服务端IPPort,也即TCPClient client = new TCPClient("127.0.0.1", 9090)
    • 启动客户端,也即client.start()

四:TCP通信示例二:多线程版本

(1)单线程版本存在的问题

上面的例子中,如果让多个客户端连接服务端会存在如下问题,以两个客户端为例

  • 客户端1连接服务端后,服务端提示“客户端1上线”
  • 客户端2连接服务端后,服务端未提示“客户端2上线”
  • 客户端1发送“客户端1”后服务端接受并正确返回
  • 客户端2发送“客户端2”后服务端似乎没有接受到消息,也没有什么反应
  • 客户端1结束运行,服务端提示“客户端1下线”,此时刚才客户端2发送的“客户端2”立刻显示同时服务端也正确回复
    在这里插入图片描述

产生这样的现象原因在于服务端整个处理逻辑中只有一个线程,所以服务端在处理客户端1的请求时会被阻塞在下面代码中

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


if (!scanner.hasNext()) {
    System.out.println("【客户端IP: " + clientSocket.getInetAddress().toString()
            + "客户端口号:" + clientSocket.getPort() + "】"
            + "下线");
    break;
}

当客户端1下线之后,服务端立马收到客户端2的请求然后才会去处理。所以要解决这个问题,整个代码逻辑必须使用多线程的方式进行改写

(2)代码

改写也比较简单,主线程持续监听客户端连接,每当一个客户端连接时便创建一个线程执行processConnection方法

public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            // 调用监听套接字的accept()连接客户端,并返回Socket类型的clientSocket
            // 将clientSocket传递给具体处理连接的方法processConnection()进行处理
            // 主线程一直负责监听客户端连接
            Socket clientSocket = listenSocket.accept();
            // 每来一个客户端使用一个线程处理
            Thread thread = new Thread(){
                @Override
                public void run(){
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            };
            thread.start();
        }
    }

(3)效果展示

如下,创建两个客户端

在这里插入图片描述

五:TCP通信示例三:线程池版本

(1)多线程版本存在的问题

采用多线程最大的问题在于当客户端数量一多就会涉及到频繁的线程创建和销毁,这开销会很大,所以为了减小创建销毁开销,同时也为了增加程序稳定性,这里我们可以使用线程池完成

  • Java线程池用法

(2)代码

改写也比较简单,主线程持续监听客户端连接,每当一个客户端连接时便使用线程池中的线程去执行processConnection方法

 public void start() throws IOException {
        System.out.println("服务器启动!");
        // 创建一个线程池
        ExecutorService service = Executors.newCachedThreadPool();
        while (true) {
            // 调用监听套接字的accept()连接客户端,并返回Socket类型的clientSocket
            // 将clientSocket传递给具体处理连接的方法processConnection()进行处理
            // 主线程一直负责监听客户端连接
            Socket clientSocket = listenSocket.accept();
            // 每来一个客户端使用线程池中的线程处理
            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

(3)效果展示

在这里插入图片描述

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

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

相关文章

量子计算(二十一):Deutsch-Josza算法

文章目录 Deutsch-Josza算法 Deutsch-Josza算法 量子算法是量子计算落地实用的最大驱动力,好的量子算法设计将更快速推动量子计算的发展。 Deutsch-Jozsa量子算法,简称D-J算法,DavidDeutsch和RichardJozsa早在1992年提出了该算法&#xff0…

分布式事务方案分析:两阶段和TCC方案(图+文)

1 缘起 补充事务相关知识过程中, 发现,默认的知识都是基于单体服务的事务,比如ACID, 然而,在一些复杂的业务系统中,采用微服务架构构建各自的业务, 就有了分布式事务的概念,比如&am…

一站式云原生体验|龙蜥云原生ACNS + Rainbond

关于 ACNS 龙蜥云原生套件 OpenAnolis Cloud Native Suite(ACNS)是由龙蜥社区云原生 SIG 推出的基于 Kubernetes 发行版本为基础而集成的套件能力,可以提供一键式部署,开箱即用,以及丰富的云原生基础能力,…

JProfiler的使用

一、安装 从https://www.ej-technologies.com/download/jprofiler/files获取,如果需要对服务器远程分析,注意服务器版本的jprofiler和windows版本一致。 二、监控一个本地进程 2.1 不使用idea 安装之后,打开jprofiler,点击红框…

电脑蓝屏并提示BAD_POOL_CALLER怎么办?

电脑蓝屏可以说是Windows的常见问题,各种各样的终止代码对应着不同的问题。如果你的蓝屏代码显示BAD_POOL_CALLER,这篇文章就是为你提供的。 可能导致BAD_POOL_CALLER蓝屏错误的原因: 1、硬件或软件不兼容 2、过时或错误的设备驱动程序 3…

DataWorks创建JavaUDF函数全流程

文章目录插件下载创建MaxCompute Studio项目创建MaxCompute Java Module编写Java UDF函数注意说明:这篇文章只是个人记录下,具体步骤都可以在官网找到。推荐看官网文档哈 插件下载 创建MaxCompute Studio项目 启动IntelliJ IDEA,在顶部菜单栏…

1806. 还原排列的最少操作步数

解法一: 根据题目的题目描述进行模拟,遇到偶数iii将arr[i]prem[i/2]arr[i] prem[i/2]arr[i]prem[i/2],遇到奇数iii,将arr[i]prem[(n−1i)/2]arr[i]prem[(n-1i)/2]arr[i]prem[(n−1i)/2] 时间复杂度: O(n2)O(n^2)O(n2), 最多会循环n次空间复杂度&#…

Nginx反向代理使用方法小总结

文章目录一、前言二、反向代理定义重申三、短网址方式代理四、多级域名方式代理五、通配符代理方式总结一、前言 本文只介绍代理转发到一个主机的方式,至于在代理时进行负载均衡大家需要自己尝试,也比较简单,在本专栏前面文章提到过&#xf…

(二)Redis概述与安装

目录 一、概述 1、特性 2、应用场景 二、安装 三、启动 1、前台启动(不推荐) 2、后台启动(推荐) 四、redis关闭 五、redis相关知识介绍 一、概述 1、特性 Redis是一个开源的key-value存储系统。和Memcached类似&#x…

TOOM舆情分析监控管理系统集成,舆情监控系统监测那些人群?

当前,互联网已成为思想文化信息的集散地和社会舆论的扩大器,舆情监控新闻、论坛博客、聚合新闻等等,做好舆情监控,至于监测那些人群,舆情分析监控是非常必要的,接下来我们简单了解TOOM舆情分析监控管理系统…

接口协议之抓包分析 TCP 协议

TCP 协议是在传输层中,一种面向连接的、可靠的、基于字节流的传输层通信协议。环境准备对接口测试工具进行分类,可以如下几类:网络嗅探工具:tcpdump,wireshark代理工具:fiddler,charles&#xf…

《移动通信》多章节部分重要习题(简答、单选、判断)

调制技术在移动通信中的作用? 调制有两个目的: 1 )经过调制可以使基带信号变换为带通信号。选择需要使用的载波频率 ( 简称载频 ) ,可以把信号的频谱从开始的频段转移到到所需要的频段上,从而使传输信号适应信道的要求,或是可以把许多个输入信号合起来应用于多路传…

开发模型 和 测试模型 详解

开发模型 开发模型 : ① 瀑布模型 ② 螺旋模型 ③ 增量模型 和 迭代模型 ④ 敏捷模型 (优点 缺点 适用场景)测试模型 : ① V模型 ② W模型瀑布模型优点/特点:线性结构,每个阶段 只执行一次是其他模型的一个基础框架缺点&#xff1…

sentinel-Roadmap(三)

Pages 60 Sentinel 官方网站 OpenSergo 微服务治理 文档 Read Me新手指南Sentinel 介绍FAQRoadmap如何使用工作原理流量控制集群流控(分布式流控)网关流控熔断降级热点参数限流系统自适应限流黑白名单控制实时监控数据动态规则控制台生产环境使用 Sent…

Spring依赖注入时,创建代理bean和普通bean详解

问题来源 以前一直有个疑惑,为什么我创建的controller中注入的service类有时候是代理类,有时候是普通javabean,当时能力不够,现在已经有了点经验就大胆跟了跟源码,看看到底咋回事。 首先看看问题现象: a1…

linux nfs umount报错:device is busy

执行nfs卸载命令umount /mnt,报错target is busy. 或device is busy可以按以下步骤检查:退出要卸载挂载的目录,再执行卸载挂载cd ../umount /mnt找出占用目录的端口,kill端口fuser -m /mnt/kill -9 端口umount /mnt停止nfs服务&am…

PCA 主成分分析-清晰详细又易懂

PCA(Principal Component Analysis)通过线性变换将原始数据变换为一组各维度线性无关的表示,可用于提取数据的主要特征分量,常用于高维数据的降维。 当然我并不打算把文章写成纯数学文章,而是希望用直观和易懂的方式叙…

Java char[]数组转成String类型(char to String)详细介绍

前言 string toCharArray() 方法将给定的字符串转换为字符序列 Java中字符串转换为字符数组的方法在之前的博客已经介绍了! 今天介绍char[]数组转成String 方法有4种: 使用 String 类的 valueOf() 方法使用字符串连接使用 Character 类的 toString() 方…

图形编辑器:场景坐标、视口坐标以及它们之间的转换

大家好,我是前端西瓜哥。 图形编辑器的坐标系有两种。 一个是场景(scene)坐标系,一个是 视口(viewport)坐标系。视口就是场景的一个子区域。 假设我们的视口的原点,离场景原点的坐标水平和垂直…

C2芯片一ESP32-C2开发板

C2是一个芯片采用4毫米x 4毫米封装,与272 kB内存。它运行框架,例如ESP-Jumpstart和ESP造雨者,同时它也运行ESP-IDF。ESP-IDF是Espressif面向嵌入式物联网设备的开源实时操作系统,受到了全球用户的信赖。它由支持Espressif以及所有…