【java网络编程】TCP三次握手、四次挥手,常见Socket通信BIO

网络编程,网络通信

内容管理

    • TCP三次握手,四次挥手
      • TCP 建立连接 --- 三次握手
      • TCP断开 ---- 四次挥手
    • java网络IO
    • BIO 同步阻塞
      • Socket通信模型
      • BIO网络通信Demo
        • 连接超时设置
        • 读超时设置
        • 写超时设置
        • BIO模式 --- 多线程
        • 封装read和write方法
        • 模板模式 -- 字符串类型Session
        • 模板模式 -- 对象类型
        • 模板模式 -- 文件类型


网络编程 — IO、socket、BIO、NIO 【redis、zookeeper、dubbo、MQ等通信】


分布式中间件Middleware可以提高项目的性能,使用MVCM模式重构之前的项目,为微服务和分布式扩展做准备,知识的广度不能野蛮盲目扩张,深度需要跟上,do you know分布式环境下各种分布式应用如何进行通信?,如何进行IO传输?RPC框架such as Dubbo如何实现的?

当然这不是计网,只是网络编程,简单谈谈各种IO和Netty框架,Netty使用NIO、而不是AIO,因为Netty看重的是在LInux上的性能,在Linux上,NIO更优

封装的framework极大简化了开发的难度,除了基础的开发工具Spring系列,还有就是各种中间件比如缓存中间件Cache、消息中间件MQ、以及Redisson等,可以配合进行相关的提高访问速度,削峰、限流、熔断,轮询; 当然削峰、布隆过滤器还可以直接借助Guava封装的开源的工具类

  请求  ---> nginx负载均衡(前台限流避免Tomcat并发)-->后台 
  												|
  		             令牌桶分发机制进行限流(rabbitMQ进行异步通信)
  		                    |
  		                 后台Service服务 <---> redis (相关的分布式)
  		                             |
  		                            MQ限流削峰(减轻IO压力)
  		                             |
  		                            异步录入数据库

可以看到这里就是设计的一个可能存在高并发的功能的一个基本的设计(录入数据库采用异步的昂是,像之前可能时开启多线程,SpringBoot开启线程简化成注解的方式),如果采用分布式中间件,那么就可以直接使用MQ,一方面还能够起到限流的作用; 在分布式环境下,如果不了解中间件的通信原理和应用的通信原理,那么发生错误时,可能会有些不知所措

首先需要明白Socket(套接字)也就是传输层的一个存储信息的本地标识,存储的信息包括原IP:port和目标的IP和Port

TCP和UDP为最基本的传输层协议: TCP基于连接的,UDP是无连接的;因此TCP更加安全可靠,对于出现问题的数据会进行重传, 而Java的Socket编程主要就是基于TCP/IP协议进行网络编程,因此先简单介绍TCP的连接和断开机制

TCP三次握手,四次挥手

we konw TCP是基于连接的传输层协议,也就是Client和Server要进行数据交换,必须先建立TCP连接,数据传输完毕之后要进行TCP链路的关闭释放

TCP 建立连接 — 三次握手

cfeng【客户机】在一个大雾天气看到了Clei同学【服务端】,Cfeng和Clei间隔一段距离,Cfeng看到很开心,但是不知道是否真的是CLei,所以先远远的挥了挥手【第一次握手,发送SYN + 自己的ISN】,Clei同学看到了挥手,很开心,对着Cfeng微笑【ACK】,但是有点疑惑Cfeng是否是在给自己招手,所以也同时挥手【SYN + 自己ISN】, Cfeng看到Clei挥手,很开心没有认错人,对着CLei微笑【ACK】,之后两者迅速靠近聊天【建立了TCP连接】

TCP 作为可靠的传输层协议,需要保证数据可靠传输(2次不行),同时也要提高传输的效率(4次不行),TCP连接的一方,操作刺痛会随机选取一个32位上的序列号Initial Sequence Number (ISN),以该初始序列号进行编号,每一个byte比如编号1,依次编号,这样可以让另外一方做好准备 ——— 什么样的数据合法,比如编号小于ISN可能不合法,同时可以确认到达的字节数(从ISN开始计算); 同理另外一方也是如此,所以关键就是要让对方知道自己的ISN

TCP连接握手,握手握的是双方的数据原点序列号ISN (SYN有数据才会重传,ACK无数据不会重传,只是一个标志)发送的TCP报文包含SYN和ISN

  1. 第一次握手: Clinet的TCP向server的TCP发送TCP报文,不包含应用层数据,SYN标志和SYN,SYN报文封装IP数据报发送给Server;【客户端 --> SYN ->服务端】ACK位ISN + 1 客户端进入SYN_SEND状态,等待确认, Server可以确认clent发送正常和自己接收正常
  2. 第二次握手: Server从IP数据报取出TCP的SYN报文段,分配一个TCP连接的缓存和变量,回复SYNACK报文段(不包含应用层数据,包含SYN,服务器的ISN,同时确认ACK); 表明收到,我也希望和你建立连接; 服务端进入SYN_RECV状态,Client确认自己和对方都发送接收正常,server确认对方发送和自己接收正常
  3. 第三次握手: 客户机收到SYNACK报文段,分配缓存和变量,发送一个确认报文段,连接确认ACK,为server的ISN + 1,表明知晓了对方的ISN;server也能够确认一切正常,自己和对方的发送接收正常

客户端状态变化: CLOSE --> SYN SEND --> ESTABLESHED

服务段: CLOSE —> SYN RECIEVE—> ESTABLESHED

相关的question:

  • 第二次握手传回了ACK,为什么还要传回SYN?

    服务端传回ACK可以告知服务器client发送正常,自己接收正常,表明从客户端到服务器的通信是正常的,回传SYN是为了建立确认服务端到客户端的通信(确认客户端是在和自己通信,主要是告知ISN — 见描述)

  • 为什么要三次,两次不行? 【client—>SYN—> server; server -->ACK–>client】

    二次握手首先就是只是确认客户端到服务器的连接正常,只能Client可以确认一切正常,server不知道自己是否可以正常发送,client是否可以正常接收; 当然防止已经失效的报文段又传到服务器, 比如client发起TCP连接请求,由于阻塞,服务器没有按时给出ACK,客户端确认超时,重传报文段,(之前的按理该失效,但是实际上只是阻塞了一会),如果只是两次握手,那么服务器就直接确认建立连接;— 建立了两个连接,浪费资源; 但是如果3次握手,还需要服务器再确认该连接,之前失效的报文client就不会确认,那么就不会建立TCP连接 也就是两次握手就建立TCP连接可能资源浪费,不安全可靠

  • 为什么不四次?

    TCP理论上不论握手多少次都不能确定一条信道绝对可靠,通过3次握手至少可以确定是可用的,继续增加次数只是提高可信程度罢了,并且会浪费资源,三次握手是确保双方能够正常手法的最低值

第一次和第二次都是确认状态,SYN报文不能携带数据,避免攻击

TCP断开 ---- 四次挥手

Cfeng 在大雾中远远看见Clei,二者确认身份,但是因故必须道别了;Cfeng有事要走了【client数据传输完成】, Cfeng 先挥手道别【FIN】,CLei看到了,痛苦的点了点头【ACK】,但是CLei还想给Cfeng比个手势【服务端可能还需要传输数据】,Cfeng就原地看着接收CLei的信息,CLei弄完了一切,CLei再次挥了挥手【FIN】,Cfeng看到了,也痛苦的点了点头【ACK】,二者分开

TCP为全双工通信,可双向传输数据,任何一方在传输结束发起连接释放通知,对方ACK就会进入半关闭状态(还可以接收),当另外一方也没有数据发送,发出连接释放通知,关闭TCP连接

可以看到其实TCP建立和断开都是需要双方同意的,TCP四次挥手就是因为server的ACK和FIN不能合并, 建立TCP连接是为进行数据传输,服务器收到client的断开请求时可能数据还没有传送完成,毕竟网络不可控,要等数据传输完成之后,Server再发起FIN报文段 【SYN和FIN都是占字节的,所以需要确认到达,如果没有收到ACK会重传FIN,而不是ACK】

  • 第一次挥手: 客户端发送FIN报文段,seq为最后的数据报文段序列号 + 1; 关闭客户端向服务端的数据传送,客户端进入FIN-WAIT-1状态;client单方面请求不再传数据,server确认client不会传数据,但自己可能还要传
  • 第二次挥手: 服务器收到FIN数据段,发送ACK(client序列号 + 1),这个时候服务端进入CLOSE-WAIT状态,客户端进入FIN-WAIT-2状态,server了解client要关闭了,但是server可能还需要发送数据,所以A不能要求B也同意马上就关闭TCP连接
  • 第三次挥手: 服务端关闭连接发送一个FIN数据段(server的序列号 + 1);这个时候服务端进入LAST-ACK状态(最后一次确认,因为客户端ACK之后就关了),服务端确认不发送数据之后,也同意关闭
  • 第四次挥手: 服务端发送ACK(server的seq + 1),到服务端进入TIME-WAIT状态,服务端收到之后进入CLOSE状态,客户端等待2MSL后未见异常,说明正常关闭,客户端也就关闭;client也确认server不会再传送数据

相关的question:

  1. 为什么不能把服务端的ACK和FIN合并变为三次挥手?

    这里和TCP建立不同,TCP建立前两次握手的报文段没有数据,所以可以直接合并,因为不会有任何影响,三次足以确保双方状态正常,四次只是徒增资源浪费; 而关闭不同, 服务器收到客户端断开请求时,可能还有数据没有发送完成,这个时候就先ACK,表明知晓,等待自己的数据发送完成再发起FIN; 确保信息不会出现丢失【三次可能让服务端消息发送没有完毕】

  2. 为什么客户端需要等待2MSL(报文段最长寿命)后再进入CLOSED状态?

    MSL(maximum segment lifetime 最大存活时间)一个segment的最大存活时间,2MSL可以保证一次传输和恢复的完成; 第四次挥手可能ACK丢失,这个时候服务端会重传FIN,再次ACK,防止server没有收到ACK不断重发FIN,无法关闭, 同时还可以比卖你失效连接请求报文出现再这次连接上 – 2MSL后都已经消失

java网络IO

java.io基于流模型实现,提供File抽象,输入输出流等IO功能,交互方式为同步阻塞(读取时不能干其他事情并且必须等待读取完成close),读取输入流或者写入输出流动作完成close之前,线程就会一直阻塞, 代码简单,但是IO效率低下,传统的inputstream,outputstream

java.net 下面提供网络API,也就是网络编程,提供了Socket接口等,基于TCP/IP协议封装,Socket、ServerSocket、HttpURLConnection也是同步阻塞IO,因为网络通信同样是IO行为

java.nio中提供了Channel、Selector、Buffer等抽象、可以构建IO多路复用,更接近操作系统底层高性能数据操作;

java.nio2中引入了异步非阻塞IO(异步IO,异步都是非阻塞的,阻塞需要亲自等待同步),AIO,基于事件和回调机制

I/O 决定使用什么样的通道进行数据的发送和接收,决定了程序通信的性能; 而同步和异步针对应用程序和内核的交互而言; 阻塞和非阻塞针对进程访问数据时,更具IO操作的就绪状态采取不同的方式

同步 :指的是用户进程触发IO操作等待(轮询)IO是否就绪; 【自己上街买衣服,亲自买,所以买衣服时不能干其他事情】

异步 :是触发IO后,进程就去做其他的事情,IO完成后再回来继续处理【委托朋友去买衣服,自己就去干别的事情】

阻塞 :当对文件描述符读写时,如果没有东西可读或者暂时不可写,那么程序就进入阻塞状态,知道有东西可读或者可写 【去银行办理业务,柜员不在,一直等到柜员回来再办理】

非阻塞: 如果没有东西可读或者不可写,那么读写函数马上返回,不会等待 【餐厅前台买单后,不再原地等待,带上提示器去干别的事情】

应用程序向操作系统发起IO请求给操作系统内核,操作系统内核需要等待程序就绪,数据可能来自别的应用或者网络,IO分为两个阶段:

  • 等待数据: 数据可能来自其他的程序或者network,如果没有数据,应用就阻塞等待
  • 拷贝数据: 将就绪的数据拷贝到应用程序工作区

Linux系统中,操作系统的IO就是recvform(),也就是recvfrom()调用分为等待和拷贝

网络IO一共有5类:

  • 阻塞IO blocking IO: BIO 用户进程发起IO操作,等待IO操作完成,必须IO操作完成后,用户进程才能完成 适用于连接数目小且固定的架构,服务器资源要求高
  • 非阻塞IO nonblocking IO: NIO 用户进程发起IO操作可返回做其他的事情,但是用户进程需要轮询IO,从而导致不必要的CPU浪费 连接数目多但是比较段(轻操作)的架构,比如聊天服务器
  • 异步IO : asyncronous IO : AIO 将整个IO操作(等待数据、拷贝数据)都交给操作系统完成,数据就绪后操作系统通知用户进行,整个过程应用程序不需要阻塞 适用于连接数目多并且连接比较长(重操作)的架构,比如相册服务器
  • 多路复用IO : IO multiplexing select epoll,事件驱动IO,select/epoll这个funciton会不断轮询所负责的所有的socket,当某个socket数据到达,通知用户进程; 通过select可以监听多个IO, 少数线程进行大量的客户端通信
  • 信号驱动IO: singnal driven IO 也是一种非阻塞IO,信号驱动方式的相比NIO不需要轮询IO就绪,被动接收信号,再调用recvfrom即可

需要到餐厅点一份美餐:

  1. 同步阻塞: 亲自到餐厅点餐,并一直等在前台,喊“好了没有”
  2. 同步非阻塞: 亲自到餐厅点餐,点完出去玩了,但是每隔一会就会到前台去问一句 好了没有
  3. 多路复用: epoll/select 出去玩的时候,饭馆打电话让回去取餐
  4. 异步(只有非阻塞): 直接点餐厅的外卖,然后就去做其他的事情了

【java网络编程】TCP三次握手、四次挥手,常见Socket通信BIO_第1张图片

需要注意的是:BIO、NIO、多路复用IO都是同步IO,异步一定是非阻塞的,真正的异步IO需要CPU的深度参与,只有用户线程操作IO完全不考虑IO执行,才是真正的异步,而fork子线程去轮询、死循环,或者select、poll、epoll都不是异步

BIO 同步阻塞

BIO需要IO操作完成才能进行其他的操作,适用于并发量小的系统(连接数目小)服务器实现模式为一个连接一个线程,客户端有连接请求时就需要启动一个线程处理,如果该连接不做任何事情,将造成不必要的开销

【java网络编程】TCP三次握手、四次挥手,常见Socket通信BIO_第2张图片

Socket通信模型

java的socket编程就是基于java封装的Socket接口等进行网络的通信IO,基本的通信模型为: 【与异步同步无关,与阻塞非阻塞无关】

【java网络编程】TCP三次握手、四次挥手,常见Socket通信BIO_第3张图片

基于TCP/IP,所以连接和关闭就需要经历三次握手和四次挥手;

客户端和服务端通信的流程:

  1. 建立通信连接: 服务端开启服务端口,等待客户端连接,客户端发起连接请求,服务段接收请求(三次握手),建立连接
  2. 进行会话(传输数据): 建立连接后,客户端和服务端之间传输业务数据,客户端发送或接收,服务端发送或者接收,会话双方可以进行多次发送和接收
  3. 关闭连接(四次挥手): 会话完毕时,各自关闭连接

BIO网络通信Demo

BIO阻塞通信主要就是利用Socket和Server进行网络的连接和通信【这里会简单演示客户端和服务端的代码,之前blog中有 70行代码通信】

服务端Server:

public class ServerSock {
    //服务端主要就是端口号即可
    private int port;

    public ServerSock(int port) {
        this.port = port;
    }

    //模拟会话
    public void session(Socket socket) {
        System.out.println("与客户端进行会话,发送或者接收消息....");
    }

    //服务代码
    public void service() {
        try {
            //ServerSocket开启服务端的socket
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("服务端打开了端口,等待连接.....");

            //轮询阻塞
            while (true) {
                Socket socket = serverSocket.accept(); //代指连接到服务端的客户端
                System.out.println("连接到客户端,开启会话...");
                this.session(socket);
                //关闭连接
                socket.close();
                ;
                System.out.println("会话结束,服务端关闭连接");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

这里的服务端为单线程的,客户端的连接的代码

public class ClientSock {
    private String serverIP;

    private int port;

    protected ClientSock(String serverIP, int port) {
        this.serverIP = serverIP;
        this.port = port;
    }

    public void session(Socket socket) throws Exception {
        System.out.println("开始会话");
    }

    public void communicate() {
        //连接对象
        InetSocketAddress inetSocketAddress = new InetSocketAddress(serverIP,port);
        Socket socket = new Socket();
        try {
            socket.connect(inetSocketAddress);
            System.out.println("客户端连接成功,开始会话");
            this.session(socket);
            socket.close();
            System.out.println("会话结束,关闭连接");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ClientSock client = new ClientSock("127.0.0.1",10086);
        client.communicate();
    }
}

socket.connect(inetSocketAddress); 和 Socket socket = serverSocket.accept();都是阻塞点,客户端或者服务端会尝试一直连接或者等待,直到连接成功或者超时, 超时分为连接超时、读超时,写超时

连接超时设置

连接超时connect中可以设置timeout,设置客户端连接的超时事件,如果超时则抛出TimeoutException; 当没有设置timeout,无限尝试,没有超时的时间,如果timeout小于TCP三次握手的时间,则Socket永远建立不起来

读超时设置

Socket连接的read擦欧总会阻塞当前的线程,直到新的数据到来或者超时异常产生,默认无限等待; 如果远程机器重启等异常,本地的read操作一直阻塞,所以必须设置read操作的读超时时间, 会抛出异常SocketTimeoutExcepton(但是socket连接依然有效)

可以通过socket.setSoTimeout()即可

写超时设置

当Socket连接的write操作发送数据时,如果远程机器异常,TCP会重传数据(可靠),如果多次重传失败则会自动关闭TCP连接

所以基于TCP协议栈的超时重传机制,写操作超时不需要单独设置

BIO模式 — 多线程

BIO模式下,服务器为每一个连接都会开启一个单独的线程处理; 同时,在高并发环境下,服务器使用一个线程处理连接请求,建立连接后,由另外一个线程专门处理会话,提高性能

public abstract class ServerSock {
    //服务端主要就是端口号即可
    private int port;

    //服务端使用线程池,每一个会话由一个线程处理
    private ExecutorService fixPool = Executors.newCachedThreadPool();

    public ServerSock(int port) {
        this.port = port;
    }

    //模拟会话
//    public void session(Socket socket) {
//        System.out.println("与客户端进行会话,发送或者接收消息....");
//    }
    abstract protected void session(Socket socket) throws Exception;

    //服务代码
    public void service() {
        try {
            //ServerSocket开启服务端的socket
            ServerSocket serverSocket = new ServerSocket(port);
            System.out.println("服务端打开了端口,等待连接.....");

            //一直轮询
            while (true) {
                Socket socket = serverSocket.accept(); //代指连接到服务端的客户端
                System.out.println("连接到客户端,开启会话...");
                //设置读超时20000ms
                socket.setSoTimeout(20000);

                //会话操作由线程池管理,另外的线程单独处理会话
                fixPool.execute(() -> {
                    try {
                        this.session(socket);
                        //关闭连接
                        socket.close();
                        ;
                        System.out.println("会话结束,服务端关闭连接");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

//    public static void main(String[] args) {
//        ServerSock server = new ServerSock(10086);
//        server.service();
//    }
}

这里主要就是服务端的会话使用线程池ExecutorService管理, 使用另外的线程进行会话,同时设置了读超时时间

主线程无限While循环,执行accept,等待客户端的连接,相当于就是一种轮询,每接受一个客户端连接,就启动一个新的线程运行次连接的会话,而main线程还是执行accept等待操作

而客户端同样设置读请求超时时间和连接超时时间,按照模板方法设计模式,将session方法变为abstract类型 【在具体子类中进行实现】

会话的交互读取可以使用相关的流即可

public void write(byte[] data) throws IOException
public void write(byte[] data,int offset,int length) throws IOException

public void read(byte[] input) throws IOException
public void read(byte[] input, int offset, int length) throws IOException

第一个read从网络通道上读取多字节填入指定的数组input,最多读取input大小,第二个是填充input的offset到 + length长度;当执行read方法时,而网络上没有字节传入,read方法会阻塞当前线程直到有字节传入

read方法返回-1 则说明InputStream关闭,发送方关闭了OutputStream导致,read方法虽然指定了字节存放的位置和预留的空间,但是网络传输的不连续性和不可预测性【 比如发送方write 10 字节,接收方调用read接收空间10字节,可能需要连续掉哦那个多次read才能完整接收】

发送方										接收方
write 10byte-->                      
										  read(10字节) 阻塞
										   
									---> 3字节
											read(7字节)
								    --> 4字节
								     		read(3字节)
								      ----> 3字节	
封装read和write方法

所以需要优化,让接收方能够完整接收到与预留空间等量字节

 /**
     * 确保接收方能万丈接收到与预留空间等量的字节
     */
    private static void readFully(Socket socket, byte[] bytes) throws Exception {
        //获取socket的输入流
        InputStream ins = socket.getInputStream();
        int byteToRead = bytes.length;
        int readCount = 0;
        //可以保证完整读取就是因为每次读取都是从上一次结束的空间开始存字节,高效利用空间
        while(readCount < byteToRead) {
            int result = ins.read(bytes,readCount,byteToRead - readCount);
            //Stream意外结束
            if(result == -1) {
                throw new Exception("InputStream意外结束");
            }
            readCount += result;
        }
    }

接收方可以预留空间,但是不知道发送方发送多少字节,所以发送方发送数据之前因该先告知字节总数, recv方法接收方读取一个整数,创建一个长度为该整数的字节数组,也就是recv会接收要传送多少个字节

/**
     * 将字节数组转化为对应的整数
     */
    public static int byteArrayToInt(byte[] b) {
        return b[3] & 0xFF |
                (b[2] & 0xFF) << 8 |
                (b[1] & 0xFF) << 16 |
                (b[0] * 0xFF) << 24;
    }


    /**
     * 接收方创建先fully读取一个整数的4字节,之后再fully读取标识该整数长度的字节数组
     */
    private static byte[] recv(Socket socket) throws Exception {
        byte[] countBytes = new byte[4];
        //调取readFully完整读取countBytes
        readFully(socket,countBytes);
        //将字节数组转化为对应的整数
        int count = byteArrayToInt(countBytes);
        byte[] dataBytes = new byte[count];
        //读取dataBytes
        readFully(socket,dataBytes);
        return dataBytes;
    }
}

recv方法是对read方法的升级,所以对应的write方法也需要升级

    /**
     * 将整数转化为对应的字节数组
     */
    public static byte[] intToByteArry(int a) {
        return new byte[]{
                (byte) ((a >> 24) & 0xFF),
                (byte) ((a >> 16) & 0xFF),
                (byte) ((a >> 8) & 0xFF),
                (byte) (a & 0xFF)
        };
    }

    /**
     * write方法升级,先发送一个用4字节表示的整数,再发送一组字节的字节数
     */
    private static void send(Socket socket, byte[] obytes) throws Exception {
        OutputStream outs = socket.getOutputStream();
        int byteCount = obytes.length;
        //先发送字节数
        byte[] cntBytes = intToByteArry(byteCount);

        outs.write(cntBytes,0,cntBytes.length);
        outs.flush();
        //再发送内容
        outs.write(obytes,0,obytes.length);
        outs.flush();
    }
}
模板模式 – 字符串类型Session

之前将服务端和客户端的session都定义为abstract类型,就是为了能够按需实现不同的类型,继承实现即可

/**
     * String类型的消息的发送和接收
     */
    public static  void sendString(Socket socket, String msg) throws Exception {
        byte[] obytes = msg.getBytes("UTF-8");
        send(socket,obytes);
    }

    public static String recvString(Socket socket) throws Exception {
        byte[] ibytes = recv(socket);
        String msg = new String(ibytes,0,ibytes.length,"UTF-8");
        return msg;
    }

之后利用这两个方法进行String通信

public class StringClient extends ClientSock {


    protected StringClient(String serverIP, int port) {
        super("127.0.0.1", 10086);
    }

    //模拟并发,这里使用线程池
    @Override
    protected void session(Socket socket) throws Exception {
        String msg = "I am Cfeng";
        System.out.println("SEND: " + msg);
        SessionStreamUtil.sendString(socket,msg);
        //接收消息
        msg = SessionStreamUtil.recvString(socket);
        System.out.println("RECEIVE: " + msg);
    }


    public static void main(String[] args) {
        ExecutorService fixPool = Executors.newCachedThreadPool();
        int concurrentCount = 100;
        //多线曾模拟
        for(int i = 0; i < concurrentCount; i ++) {
            fixPool.execute(() -> {
                StringClient client = new StringClient("127.0.0.1",10086);
                client.communicate();
            });
        }
        fixPool.shutdown();
    }


}

之后就是服务器的String的实现

public class StringServer extends ServerSock {


    public StringServer(int port) {
        super(10086);
    }

    @Override
    protected void session(Socket socket) throws Exception {
        String msg = SessionStreamUtil.recvString(socket);
        System.out.println("RECEIVE: " + msg);
        msg = "wo shi ni";
        SessionStreamUtil.sendString(socket,msg);
        System.out.println("SEND: " + msg);
    }

    public static void main(String[] args) {
        StringServer server = new StringServer(10086);
        server.service();
    }
}

这里只是测试,就发送完就可,但是在发送的时候就是阻塞状态,只能被动等待消息的进入之后读取

模板模式 – 对象类型

如果通信双方想要直接传输通信对象,接收方和发送方必须使用相同的方法,网络通信协议底层传输的是字节,将Java对象转为字节序列发送,接收时再转换即可; 这里的对象必须实现序列化才能进行网络传输

【传输对象还可以直接使用ObjectOutputStream等流,对象也必须序列化; 将java对象转为JSON字符串,按照String进行传递,接收时再转化为对象; 使用其他的序列化方法】

/**
     * Object类型的消息发送和接收
     */
    public static byte[] objSerializableToByteArray(Object objectSerializable) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        //直接转化
        objectOutputStream.writeObject(objectSerializable);
        byte[] bytes = byteArrayOutputStream.toByteArray();

        objectOutputStream.close();
        byteArrayOutputStream.close();
        return bytes;
    }

    public static Object byteArrayToObjSerializable(byte[] bytes) throws Exception {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);

        Object object = objectInputStream.readObject();
        //IO为阻塞,必须关闭
        objectInputStream.close();
        byteArrayInputStream.close();
        return object;
    }

    public static void sendObject(Socket socket, Object object) throws Exception {
        byte[] bytes = objSerializableToByteArray(object);
        send(socket,bytes);
    }

    public static Object recvObject(Socket socket) throws Exception {
        byte[] bytes = recv(socket);

        Object object = byteArrayToObjSerializable(bytes);
        return object;
    }

而客户端和服务端需要改动的地方就是session中发送的消息 和 发送接收的方法

模板模式 – 文件类型

基于BIO进行文件会话,发送文件时要先发送文件名称,再发送文件;接收时也是先接收名称再接收文件

字节数组下标为整数类型,文件长度为长整数类型,所以对于超大文件,一次性读取文件全部内容写入字节数组是不可行的, 必须分批次读取文件内容,每次读取一组字节发送出去,直到读取完毕所有的字节, 接收方也同样只能分批次接收;需要将文件总长度传递 验证文件传输完成

/**
     * 文件传输
     */
    public static byte[] longToByteArray(long x) {
        ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.putLong(0,x);
        return buffer.array();
    }

    public static long byteArrayToLong(byte[] bytes) {
        ByteBuffer buffer = ByteBuffer.allocate(8);
        buffer.put(bytes,0,bytes.length);
        buffer.flip();
        return buffer.getLong();
    }

    public static void sendFile(Socket socket, String localFileName) throws Exception {
        OutputStream os = socket.getOutputStream();

        File file = new File(localFileName);
        long fileLength = file.length();
        //发哦那个文件总的字节数,java的long为8字节
        byte[] lengthBytes = longToByteArray(fileLength);
        os.write(lengthBytes);
        os.flush();

        FileInputStream fis = new FileInputStream(file);
        //缓冲区1000 * 1000 字节
        int bufSize = 1000 * 1000;
        byte[] buffer = new byte[bufSize];
        //从文件中读取一组字节
        int readLength = fis.read(buffer);
        //还要继续读,直到文件结尾
        while(readLength != -1) {
            //发送一组字节
            os.write(buffer,0,readLength);
            os.flush();
            //下一组
            readLength = fis.read(buffer);
        }
        fis.close();
    }

    public static void recvFile(Socket socket,String savedFileName) throws Exception {
        InputStream is = socket.getInputStream();
        FileOutputStream fos = new FileOutputStream(new File(savedFileName));
        //先获取一个8字节,表示文件长度
        byte[] lengthByte = new byte[8];
        readFully(socket,lengthByte);
        //要接收的总字节
        long restBytesToRead = byteArrayToLong(lengthByte);
        //缓冲去1M
        int bufSize = 1024 * 1024;
        byte[] dataBytes = new byte[bufSize];

        do {
            //接收最后一组调整
            if(bufSize > restBytesToRead) {
                bufSize = (int) restBytesToRead;
                dataBytes = new byte[bufSize];
            }
            //填满缓冲区
            readFully(socket,dataBytes);

            fos.write(dataBytes);

            restBytesToRead -= bufSize;
        } while (restBytesToRead > 0);
        fos.close();
    }
}

这里发送的时候就先发送名称

	String fileName = recvString(socket);
	System.out.println("接收文件名");
	String filePath  = "D:\\ServerFiles\\FromClient-" + fileName;
	//接收文件
	recvFile(socket,filePath);

文件操作时为了避免多线程并发操作一个文件,所以Client就不使用多线程了(线程池方式这里)这就是传统的BIO — 同步阻塞,在读取文件时线程不能做其他的事情,并且一个连接对应一个线程

你可能感兴趣的:(计算机网络养成,java,网络,tcp/ip,spring,boot,网络安全)