回显服务器(基于TCP)

news2024/10/6 14:35:28

目录

API学习

ServerSocket

Socket

服务端

思路分析

具体实现

客户端

思路分析

具体实现

运行测试

问题分析 

修改优化

完整代码


在学习了基于UDP实现的回显服务器后,我们学习基于TCP实现的回显服务器

API学习

ServerSocket

ServerSocket是创建TCP服务端Socket的API

构造方法:

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

常用方法:

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

Socket

Socket是客户端Socket或服务端中接收到客户端连接(accept方法)的请求后,返回的服务端Socket

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

构造方法:

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

常用方法:

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

服务端

思路分析

对于服务端,要实现的内容有:

1. 与客户端建立连接

2. 接收客户端发送的请求、读取解析请求

3. 根据请求计算数据响应

4. 将响应返回给客户端

由于服务器要等到客户端发送请求时才能进行接收、解析、计算响应等操作,而服务器不知道客户端什么时候发送请求,因此服务器需要一直“待命”,等待客户端发送请求 

具体实现

1.首先我们需要创建一个ServerSocket对象,并通过构造方法来指定服务器要绑定的端口号

import java.io.IOException;
import java.net.ServerSocket;

public class TcpEchoServer {
    private ServerSocket socket = null;
    public TcpEchoServer(int port) throws IOException {
        socket = new ServerSocket(port);
    }
}

2.接下来,我们实现客户端与服务端连接的建立

TCP是有连接的,因此,在进行通信之前,客户端和服务器之间需要建立连接(就像打电话一样,需要一端拨号,另一端接听后,双方才能进行通话)

除了内核建立连接外,还需要服务端进行“接听”(accept操作),才能进行通信

    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true){
            //通过accept方法来“接听”
            Socket clientSocket = socket.accept();
        }
    }

3. 然后通过实现processConnection方法来处理每一次连接建立后的通信(客户端与服务器之间的多次请求响应交互)

此时的实现过程与 基于UDP实现的回显服务器类似,循环读取请求、接收请求并解析、根据请求计算响应最后将响应返回给客户端

需要注意的是,TCP是面向字节流的,传输的基本单位是字节

    //处理连接建立后客户端与服务器之间的多次请求响应
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            //循环读取请求
            while (true){
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){//读取完毕,断开连接
                    System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //读取请求并解析
                String request = scanner.next();
                //根据请求计算响应
                String response = process(request);
                //将响应返回给客户端
                //由于直接通过outputStream进行写入不方便在响应末尾添加\n
                //因此可以使用PrintWriter进行写入(使用其中的println方法)
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                //不要忘记刷新操作
                printWriter.flush();
                //打印日志,观察程序执行效果
                System.out.printf("[%s:%d] req: %s, resp: %s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
                        request, response);
            }

        }catch (IOException e){
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }
    }
    
    //根据请求计算响应
    public String process(String request) {
        return request;
    }

客户端

思路分析

 对于客户端需要实现的内容有:

1. 从控制台读取用户输入的内容

2. 将内容构造成TCP请求,并发送给服务器

3. 等待服务器响应,当接收到服务器响应时,解析响应内容

4. 显示响应内容

具体实现

1.首先我们创建一个Socket对象,并在构造方法中传入服务器的ip和端口号

import java.io.IOException;
import java.net.Socket;

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        socket = new Socket(serverIp, serverPort);
    }
}

2. 接下来我们实现客户端的启动(循环读取请求、发送请求、读取响应最后打印响应内容)

    public void start(){
        System.out.println("启动客户端");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()){
            Scanner scanner = new Scanner(System.in);//从控制台读取要发送的请求数据
            Scanner scannerNetwork = new Scanner(inputStream);//从服务器读取响应
            PrintWriter writer = new PrintWriter(outputStream);//通过PrintWriter进行写入操作
            while (true){
                //从控制台读取请求数据
                System.out.print("请输入:");
                if(!scanner.hasNext()){//读取完毕,退出循环
                    break;
                }
                String request = scanner.next();//读取请求
                //将请求发送给服务器
                writer.println(request);//使用println方法来发送数据,使请求末尾带有\n
                writer.flush();//刷新缓冲区,使数据及时发送出去
                //从服务器读取响应
                String response = scannerNetwork.next();
                //显示响应内容
                System.out.println(response);
                
                
            }
        }catch (IOException e){
            throw new RuntimeException(e);
        }
    }

运行测试

在编写完代码后,我们同时运行服务器和客户端,并输入请求观察代码是否存在问题:

启动服务器:

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

启动客户端:

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

运行测试结果:

服务器:

客户端:

 再运行一个客户端:

运行结果:

此时第二个客户端无响应,当关闭第一个客户端后,此时第二个客户端才能正常工作

问题分析 

为什么会出现这种情况呢?

通过观察服务器代码,我们可以发现:当第一个客户端与服务器建立连接后,服务器就进入processConnection,此时会在scanner.hasNext 阻塞,等待客户端的请求,接收请求后,解析计算响应并返回,然后再次等待请求....,直到该客户端退出后,才能结束processConnection方法,再次进行“接听”

因此,当有新的客户端与服务器建立连接时,虽然新的客户端与服务器在内核层面建立了TCP连接,但服务端未“接听”,因此连接未成功建立,也就无法进行交互。第二个客户端发送的请求存储在服务器的接收缓冲区中,当第一个客户端退出后,服务器就会立即处理第二个客户端之前发送的请求

那应该如何修改代码,使得服务器能够同时与多个客户端建立连接呢?

修改优化

此时使用单线程已经无法满足我们的需求,因此我们考虑使用多线程,主线程负责执行accecpt,每当有一个客户端进行连接,就分配一个新的线程,由这个新线程为客户端提供服务

    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true){
            //通过accept方法来“接听”
            Socket clientSocket = socket.accept();
            Thread woker = new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            woker.start();
        }
    }

此时服务器就能够处理多个客户端的请求了

然而,当客户端比较多时,服务器就会频繁地创建和销毁线程,此时,我们可以考虑使用线程池

完整代码

服务端代码:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.InetAddress;
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 socket = null;
    public TcpEchoServer(int port) throws IOException {
        socket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("启动服务器");
        ExecutorService pool = Executors.newCachedThreadPool();
        while (true){
            //通过accept方法来“接听”
            Socket clientSocket = socket.accept();
           /* Thread woker = new Thread(()->{
                try {
                    processConnection(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            woker.start();*/
            pool.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());
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()){
            //循环读取请求
            while (true){
                Scanner scanner = new Scanner(inputStream);
                if(!scanner.hasNext()){//读取完毕,断开连接
                    System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //读取请求并解析
                String request = scanner.next();
                //根据请求计算响应
                String response = process(request);
                //将响应返回给客户端
                //由于直接通过outputStream进行写入不方便在响应末尾添加\n
                //因此可以使用PrintWriter进行写入(使用其中的println方法)
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(response);
                //不要忘记刷新操作
                printWriter.flush();
                //打印日志,观察程序执行效果
                System.out.printf("[%s:%d] req: %s, resp: %s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
                        request, response);
            }

        }catch (IOException e){
            throw new RuntimeException(e);
        }finally {
            clientSocket.close();
        }
    }

    //根据请求计算响应
    public String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9019);
        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 {
        socket = new Socket(serverIp, serverPort);
    }
    public void start(){
        System.out.println("启动客户端");
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()){
            Scanner scanner = new Scanner(System.in);//从控制台读取要发送的请求数据
            Scanner scannerNetwork = new Scanner(inputStream);//从服务器读取响应
            PrintWriter writer = new PrintWriter(outputStream);//通过PrintWriter进行写入操作
            while (true){
                //从控制台读取请求数据
                System.out.print("请输入:");
                if(!scanner.hasNext()){//读取完毕,退出循环
                    break;
                }
                String request = scanner.next();//读取请求
                //将请求发送给服务器
                writer.println(request);//使用println方法来发送数据,使请求末尾带有\n
                writer.flush();//刷新缓冲区,使数据及时发送出去
                //从服务器读取响应
                String response = scannerNetwork.next();
                //显示响应内容
                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", 9019);
        client.start();
    }
}

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

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

相关文章

I.MX6U C语言运行环境构建及驱动开发格式

1.设置处理器模式 设置6ULL处于SVC模式下。设置下CPSR寄存器的bit4-0,也就是M[4:0]为100110x13.。读写状态寄存器需要用到MRS和MSR指令。MRS将CPSR寄存器数据读出到通用寄存器里面,MSR指令将通用寄存器的值写入到CPSR寄存器里面去。 2.设置SP指针 SP可以指向内部…

【MySQL进阶之路】亿级数据量表SQL调优实战

欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送! 在我后台回复 「资料」 可领取编程高频电子书! 在我后台回复「面试」可领取硬核面试笔记! 文章导读地址…

go 语言爬虫库goquery介绍

文章目录 爬虫介绍goquery介绍利用NewDocumentFromReader方法获取主页信息Document介绍通过查询获取文章信息css选择器介绍goquery中的选择器获取主页中的文章链接 爬取总结 爬虫介绍 爬虫,又称网页抓取、网络蜘蛛或网络爬虫,是一种自动浏览互联网并从网…

【Tauri】(2):使用Tauri应用开发,使用开源的Chatgpt-web应用做前端,使用rust 的candle做后端,本地运行小模型桌面应用

视频演示地址 https://www.bilibili.com/video/BV17j421X7Zc/ 【Tauri】(2):使用Tauri应用开发,使用开源的Chatgpt-web应用做前端,使用rust 的candle做后端,本地运行小模型桌面应用 1,做一个免…

【Java程序设计】【C00268】基于Springboot的CSGO赛事管理系统(有论文)

基于Springboot的CSGO赛事管理系统(有论文) 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的赛事管理系统 本系统分为系统功能模块、管理员功能模块、参赛战队功能模块以及合作方功能模块。 系统功能模块:在系…

JavaI/O流 File类(文件)

目录 File类实例 File类 Java的File类是java.io.File的一个类,它表示文件或目录的路径名。这个类在处理文件和目录时非常有用,它提供了很多静态方法来操作文件和目录。 以下是一些File类的常见方法: 构造方法:创建表示文件或目…

MySQL篇----第二十一篇

系列文章目录 文章目录 系列文章目录前言一、什么是乐观锁二、什么是悲观锁三、什么是时间戳四、什么是行级锁前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 一、…

Docker 一小时从入门到实战 —— Docker commands | Create your own image | vs VM ... 基本概念扫盲

Docker crash course 文章目录 Docker crash course1. What and Why of Docker?2.1 What2.2 What problem does it solve?2.2.1 before containers2.1.2 with containers 2. Docker vs Virtual Machines2.1 Difference2.2 Benefits 3. Install docker locally4. Images vs Co…

[1-docker-01]centos环境安装docker

官方参考文档 可以在官方docker桌面版本指导文档里找到适合自己的电脑平台进行参考,或者你是老司机的话直接自己上车。 如果不需要桌面版,也可以在官方docker engine版本指导文档里找到适合自己的平台进行参考,同样,老司机可以自…

全面详细对比@Resource和@Autowired

版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl Resource和Autowired概述 在Java的Spring框架中,Resource和Autowired都是用于实现依赖注入(Dependency Injection, DI)的重要注解。依赖…

《Linux 简易速速上手小册》第8章: 安全性与加固(2024 最新版)

文章目录 8.1 防火墙与安全策略8.1.1 重点基础知识8.1.2 重点案例:配置 iptables 以保护 Web 服务器8.1.3 拓展案例 1:使用 firewalld 配置动态防御区域8.1.4 拓展案例 2:配置 ufw 以简化管理 8.2 SSH 安全最佳实践8.2.1 重点基础知识8.2.2 重…

《动手学深度学习(PyTorch版)》笔记8.6

注:书中对代码的讲解并不详细,本文对很多细节做了详细注释。另外,书上的源代码是在Jupyter Notebook上运行的,较为分散,本文将代码集中起来,并加以完善,全部用vscode在python 3.9.18下测试通过&…

LayoutInflater源码解析及常见相关报错分析

在日常Android开发中,最经常使用的RecyclerView控件是大家都绕不开的,而编写其Adapter时更离不开LayoutInflater的调用。当然,如果你做这一行有些时日了,相信你对其使用一定是炉火纯青了。即使如此,我觉得LayoutInflat…

【C++】STL之string 超详解

目录 1.string概述 2.string使用 1.构造初始化 2.成员函数 1.迭代器 2.容量操作 1.size和length 返回字符串长度 2.resize 调整字符串大小 3.capacity 获得字符串容量 4.reserve 调整容量 5.clear 清除 6.empty 判空 3.string插入、追加 、拼接 1.运算…

LeetCode:67.二进制求和

67. 二进制求和 - 力扣(LeetCode) 又是一道求和题,% / 在求和的用途了解了些, 目录 题目: 思路分析: 博主代码: 官方代码: 每日表情包: 题目: 思路分析&#xf…

第五课[lmdeploy]作业 +第六课[OpenCompass评测]作业

第五课基础作业 如下图,采用api_server部署,并转发端口通过curl提交内容。 第六课基础作业 完了捏?

【Java程序设计】【C00252】基于Springboot的实习管理系统(有论文)

基于Springboot的实习管理系统(有论文) 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的实习管理系统 本系统分为前台功能模块、管理员功能模块、教师功能模块、学生功能模块以及实习单位功能模块。 前台功能模块&#xf…

【每日一题】LeetCode——反转链表

📚博客主页:爱敲代码的小杨. ✨专栏:《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 ❤️感谢大家点赞👍🏻收藏⭐评论✍🏻,您的三连就是我持续更新的动力❤️ 🙏小杨水平有…

网络协议与攻击模拟_17HTTPS 协议

HTTPShttpssl/tls 1、加密算法 2、PKI(公钥基础设施) 3、证书 4、部署HTTPS服务器 部署CA证书服务器 5、分析HTTPS流量 分析TLS的交互过程 一、HTTPS协议 在http的通道上增加了安全性,传输过程通过加密和身份认证来确保传输安全性 1、TLS …