Java 网络编程(二)—— TCP流套接字编程

TCP 和 UDP 的区别

在传输层,TCP 协议是有连接的,可靠传输,面向字节流,全双工
而UDP 协议是无连接的,不可靠传输,面向数据报,全双工

有连接和无连接的区别是在进行网络通信的时候,通信双方有没有保存对端的地址信息,即假设 A 和 B 进行通信,A 保存了 B 的地址信息,B 也保存了 A 的地址信息,此时双方都知道和谁建立了连接,这就是有连接的通信,在之前的 UDP 数据报套接字编程中就提到过 UDP 是无连接的,所以在发送数据报的时候要加上对端的信息,防止丢包。

可靠传输是通过各种手段来防止丢包的出现,而不可靠传输则没有做任何处理直接把数据报传输过去,但是可靠传输不意味着能 100% 把数据报完整无误地传输给对方,只是尽可能降低丢包发生的概率,并且可靠传输是要使用很多手段来保持的,所以付出的代价相比于不可靠传输要大。

面向字节流就是以字节为单位来进行数据的传输,面向数据报就是以数据报为单位进行数据的传输。

全双工就是通信的双发可以同时给对方发送数据,但是半双工是指双方只有一方可以发送数据。

TCP流套接字 API 介绍

ServerSocket

ServerSocket 是TCP服务端Socket 的API

构造方法:

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

ServerSocket 方法:

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

Socket

Socket 是客户端Socket 或者是 服务端那边收到客户端建立连接的请求(通过 accept() 方法)返回的Socket 对象。

不管是客户端还是服务端的Socket 对象,他们都保留了对端的地址信息,这也是TCP协议有连接的体现。

Socket 构造方法:

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

Socket 方法:

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

回显服务器

首先在回显服务器的构造方法里初始化我们的ServerSocket

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

然后就是服务器启动运行的代码了:在面对多个客户端的时候,我们可以使用线程池来进行处理。
这里使用Executors.newCachedThreadPool()是不固定线程的个数的线程池,这样可以灵活地处理多个客户端的请求。

    public void start() throws IOException {
        System.out.println("服务器启动...");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while(true) {
            //与客户端建立连接
            Socket clientSocket = serverSocket.accept();
            //处理客户端发出的多个请求
            executorService.submit(() -> {
                try {
                    processClient(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

处理请求

我们通过了一个方法processClient来封装了处理请求的逻辑

如何进行数据的获取和写入操作?
可以通过输入流和输出流来处理getInputStreamgetOutputStream

try(InputStream inputStream = clientSocket.getInputStream();
   	OutputStream outputStream = clientSocket.getOutputStream()) 

为了更加方便地使用这两个流对象,我们进行了进一步的封装:

//对输入流和输出流进行进一步的封装,方便我们的使用
Scanner scanner = new Scanner(inputStream);
PrintWriter writer = new PrintWriter(outputStream);

由于客户端可能发来的不止一个请求,我们可以使用循环来处理一下,在循环体中,我们处理请求有三个步骤,首先获取请求解析请求,然后计算响应,最后发送响应

            while(true) {
                if(!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //解析请求
                String request = scanner.next();
                //计算响应
                String response = process(request);
                //发送响应
                writer.println(response);
                //因为此时的响应数据还在缓存区里,所以需要使用 flush 来将内存的数据发送出去
                writer.flush();
                System.out.printf("[%s:%d] request:%s response:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
                        request,response);
            }

由于这里是回显服务器,所以计算响应的代码是直接返回字符串就可以了

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

最后当客户端没有请求的时候,我们需要断开此次连接,释放资源,避免资源的泄漏

 finally {
            //当请求处理完的时候记得关闭服务器与客户端的连接,防止资源泄漏
            clientSocket.close();
        }

最终代码

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;

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

    public void start() throws IOException {
        System.out.println("服务器启动...");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while(true) {
            //与客户端建立连接
            Socket clientSocket = serverSocket.accept();
            //处理客户端发出的多个请求
            executorService.submit(() -> {
                try {
                    processClient(clientSocket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

    private void processClient(Socket clientSocket) throws IOException {
        //获取输入流和输出流
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress(),clientSocket.getPort());
            //对输入流和输出流进行进一步的封装,方便我们的使用
            Scanner scanner = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while(true) {
                if(!scanner.hasNext()) {
                    System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress(),clientSocket.getPort());
                    break;
                }
                //解析请求
                String request = scanner.next();
                //计算响应
                String response = process(request);
                //发送响应
                writer.println(response);
                //因为此时的响应数据还在缓存区里,所以需要使用 flush 来将内存的数据发送出去
                writer.flush();
                System.out.printf("[%s:%d] request:%s response:%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),
                        request,response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //当请求处理完的时候记得关闭服务器与客户端的连接,防止资源泄漏
            clientSocket.close();
        }
    }

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

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

客户端

首先在客户端构造方法建立于服务器的连接:

    public TcpEchoClient(String serverIP, int port) throws IOException {
        //与服务器建立连接
        socket = new Socket(serverIP,port);
    }

运行逻辑

首先用户从控制台输入数据,然后发送请求,接着等待服务器的响应并接收响应然后打印响应的内容即可。

    public void start() {
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            //对输入流和输出流进行进一步的封装
            Scanner scanner = new Scanner(System.in);
            Scanner scanner2 = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while(true) {
                //发送多个请求和接收多个响应
                if(!scanner.hasNext()) {
                    break;
                }
                //发送请求
                String request = scanner.next();
                writer.println(request);
                writer.flush();
                //接收响应
                String response = scanner2.next();
                System.out.println(response);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

这里要注意用户通过控制台输入数据,我们要使用的是Scanner(System.in)
当我们要发送数据的时候是使用 Socket 的 getOutputStream 方法来获取对应的输出流对象,为了便于使用所以我们又使用 PrintWriter 来进一步封装输出流,来打印响应
在发送请求的时候我们需要使用 Socket 的 getInputStream 方法来获得输入流对象,为了方便使用,所以使用Scanner(inputStream)进一步封装。

最终代码

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;

    public TcpEchoClient(String serverIP, int port) throws IOException {
        //与服务器建立连接
        socket = new Socket(serverIP,port);
    }

    public void start() {
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            //对输入流和输出流进行进一步的封装
            Scanner scanner = new Scanner(System.in);
            Scanner scanner2 = new Scanner(inputStream);
            PrintWriter writer = new PrintWriter(outputStream);
            while(true) {
                //发送多个请求和接收多个响应
                if(!scanner.hasNext()) {
                    break;
                }
                //发送请求
                String request = scanner.next();
                writer.println(request);
                writer.flush();
                //接收响应
                String response = scanner2.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", 9090);
        client.start();
    }
}

细节说明

在我们使用PrintWriter 的 writer.println(xxx)之后,我们的数据其实还保留在缓存区中,也就是还没发出去,我们需要通过flush() 方法来刷新缓存区的数据,才能将数据真正发送到对端去。


我们不可以使用writer.print这种没有自动添加换行符的方法,因为我们在接收数据的时候,使用的是Scanner 的 next()方法,next() 是要接收到空白符(包括换行符,制表符,翻页符…)才停止接收的,如果你使用 print 来发送数据,这时候的数据是没有带任何空白符的,那么就不会停止接收数据而是继续等待空白符的到来,这时候服务器就无法处理客户端的请求:如下图:

服务器就阻塞在 下图标红的代码里:
在这里插入图片描述

客户端被阻塞在接收响应的代码里:
在这里插入图片描述


你在客户端的控制台输入的回车不算进数据的换行符里,控制台输入的回车时,只是将数据交给了客户端程序,并不会自动将这些数据转换为网络流中的换行符。

换一句话说,控制台的回车只是结束你在控制台的输入,并不会自动在数据末尾加上换行符

效果展示

在这里插入图片描述
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

YOLOv8改进,YOLOv8通过RFAConv卷积创新空间注意力和标准卷积,包括RFCAConv, RFCBAMConv,二次创新C2f结构,助力涨点

摘要 空间注意力已广泛应用于提升卷积神经网络(CNN)的性能,但它存在一定的局限性。作者提出了一个新的视角,认为空间注意力机制本质上解决了卷积核参数共享的问题。然而,空间注意力生成的注意力图信息对于大尺寸卷积核来说是不足够的。因此,提出了一种新型的注意力机制—…

Python调用API翻译Excel中的英语句子并回填数据

一、问题描述 最近遇到一个把Excel表中两列单元格中的文本读取,然后翻译,再重新回填到单元格中的案例。大约有700多行,1400多个句子,一个个手动复制粘贴要花费不少时间,而且极易出错。这时,我们就可以请出…

虚拟化数据恢复—ESX SERVER共享的STORAGE中分区表被清零的数据恢复案例

虚拟化数据恢复环境&故障: 某单位信息管理平台,数台VMware ESX SERVER共享一台某品牌DS4100存储。 vc报告虚拟磁盘丢失,管理员ssh到ESX中执行fdisk -l查看磁盘,发现STORAGE中的分区表不见了。重启所有设备后,ESX S…

无效的目标发行版17和无法连接Maven进程问题

起因:我clean了一个模块的Maven想要重新下,他就开始报错。两次了都是这样。如果和我一样一开始都是好好的,直接找Maven的设置,在运行程序改,jre变成了11.它自己变成了我其他的jdk

在Docker环境下为Nginx配置HTTPS

前言 配置HTTPS已经成为网站部署的必要步骤。本教程将详细介绍如何在Docker环境下为Nginx配置HTTPS,使用自签名证书来实现加密通信。虽然在生产环境中建议使用权威CA机构颁发的证书,但在开发测试或内网环境中,自签名证书是一个很好的选择。 …

英伟达基于Mistral 7B开发新一代Embedding模型——NV-Embed-v2

我们介绍的 NV-Embed-v2 是一种通用嵌入模型,它在大规模文本嵌入基准(MTEB 基准)(截至 2024 年 8 月 30 日)的 56 项文本嵌入任务中以 72.31 的高分排名第一。此外,它还在检索子类别中排名第一(…

24 年第十届数维杯国际数模竞赛赛题浅析

本次万众瞩目的数维杯国际大学生数学建模赛题已正式出炉,无论是赛题难度还是认可度,该比赛都是数模届的独一档,含金量极高,可以用于综测加分、保研、简历添彩等各方面。考虑到大家解题实属不易,为了帮助大家取得好成绩…

SOP搭建:企业标准化操作程序构建与实施指南

一、引言 在当今充满竞争的商业领域,实现企业运营的标准化、高效化和高质量化是提升企业市场竞争力的关键所在。标准操作程序(SOP)作为一种至关重要的管理工具,能够清晰地阐述业务流程,规范操作行为,并促进…

【每日刷题】Day156

【每日刷题】Day156 🥕个人主页:开敲🍉 🔥所属专栏:每日刷题🍍 🌼文章目录🌼 1. 1020. 飞地的数量 - 力扣(LeetCode) 2. 1765. 地图中的最高点 - 力扣(LeetCode) 3. 1162. 地图分…

J.U.C - 深入解读Condition条件变量原理源码

文章目录 Pre概述Condition 主要方法Condition案例Condition的源码解析1. 等待:condition. await2. 唤醒Condition. signal Condition总结 Pre J.U.C - 深入解析ReentrantLock原理&源码 概述 配合synchronized同步锁在同步代码块中调用加锁对象notify和wait方…

c++ 类和对象(中)

前言 我们看看下面的代码以及代码运行结果 代码1 我们可以看到在我们的类Data中的函数成员print中,我们并没有设置形参,在调用此函数时,也并没有多余传参,但是我们调用它时,却能准确打印出我们的_year、_month、_day…

python:用 sklearn 构建 K-Means 聚类模型

pip install scikit-learn 或者 直接用 Anaconda3 sklearn 提供了 preprocessing 数据预处理模块、cluster 聚类模型、manifold.TSNE 数据降维模块。 编写 test_sklearn_3.py 如下 # -*- coding: utf-8 -*- """ 使用 sklearn 构建 K-Means 聚类模型 "&…

使用python编写工具:快速生成chrome插件相关文件结构

本文将详细分析一段用 wxPython 编写的 Python 应用程序代码。该程序允许用户创建一些特定文件并将它们保存在指定的文件夹中,同时也能够启动 Google Chrome 浏览器并打开扩展页面,自动执行一些操作。 C:\pythoncode\new\crxiterationtaburl.py 全部代码…

使用Web Components构建模块化Web应用

💓 博客主页:瑕疵的CSDN主页 📝 Gitee主页:瑕疵的gitee主页 ⏩ 文章专栏:《热点资讯》 使用Web Components构建模块化Web应用 使用Web Components构建模块化Web应用 使用Web Components构建模块化Web应用 引言 Web Co…

谷歌浏览器的自动翻译功能如何开启

在当今全球化的网络环境中,能够流畅地浏览不同语言的网页是至关重要的。谷歌浏览器(Google Chrome)提供了一项强大的自动翻译功能,可以帮助用户轻松跨越语言障碍。本文将详细介绍如何开启和使用谷歌浏览器的自动翻译功能&#xff…

算法---解决“汉诺塔”问题

# 初始化步骤计数器 i 1 # 定义移动盘子的函数 def move(n, mfrom, mto): global i # 使用全局变量i来跟踪步骤 print("第%d步:将%d号盘子从%s->%s" % (i, n, mfrom, mto)) # 打印移动步骤 i 1 # 步骤计数器加1 #第一种方法 # 定义汉诺塔问题的递归…

uniapp对接极光推送,实现消息推送功能

通过集成JG-JPush和JG-JCore插件,可以在应用中添加消息推送功能,向用户发送通知、消息等。这对于提升用户体验、增加用户粘性非常有帮助‌。 效果图: 一、登录极光官网 进入【服务中心】-【开发者平台】 创建应用:【概览】- 【创…

redis高性能键值数据库技术简介

什么是redis redis是远程字典服务(Remote Dictionary Server )的简写,是一个完全开源的高性能的Key-Value数据库,提供了丰富的数据结构如string、Hash、List、SetSortedset等等。数据是存在内存中的,同时Redis支持事务…

进程信号

目录 信号入门 1. 生活角度的信号 2. 技术应用角度的信号 3. 注意 4. 信号概念 5. 用kill -l命令可以察看系统定义的信号列表 6. 信号处理常见方式概览 产生信号 1. 通过终端按键产生信号 Core Dump 2. 调用系统函数向进程发信号 3. 由软件条件产生信号 4. 硬件异…

NotePad++中安装XML Tools插件

一、概述 作为开发人员,日常开发中大部的数据是标准的json格式,但是对于一些古老的应用,例如webservice接口,由于其响应结果是xml,那么我们拿到xml格式的数据后,常常会对其进行格式化,以便阅读。…