ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

TCP通信原理-Socket套接字编程实现一个简单的客服聊天功能(一)

2021-03-29 18:59:23  阅读:122  来源: 互联网

标签:socket TCP println new 接字 客户端 服务端 Socket


TCP通信原理-Socket套接字编程实现一个简单的客服聊天功能

TCP和UDP

在https的传输过程中,有TCP的三次握手,而且服务器之间的每一次交流,凡是从应用层或者从传输层开始的请求,都会经过传输层(经过上层必定经过下层,经过下层可以不经过上层),就必然会用到TCP或者UDP协议。
TCPUDP就是基于Socket概念上拓展出的传输协议
区别就是
TCP是基于连接的可靠的有序的,重量级的,适合重要文件等
UDP是无连接的不可靠的无序的,轻量级的,允许丢包,适合视频、网络电话等

Socket含义

戛然TCP和UDP都是基于Socket这个概念延伸的协议,那么Socket是什么?
它其实是一个抽象概念,代表网络通信中的一个对象。
而网络中往往一个对象的确认锁定需要两个元素–IPPort(端口)。
所以从代码角度讲,就是一个主要由ip地址和端口组成的通信对象
Socket也有两种类型,基于流的Stream Socket,对应TCP,基于报文的Datagram Socket,对应UDP。

基于TCP的Socket客服聊天室实例

我们以TCP协议的Socket为例,做一个简单聊天室功能,有了这个例子,会对TCP通信过程有一个实际的概念。
代码的逻辑流程我尽可能详细的写在注释中,感兴趣的朋友,最好实际运行操作一下,对功能做一些拓展,会加深理解。

源码及其注释

服务端如下

package com.test.sf.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
/**
 * 服务端
 */
public class SocketServerDemo {
    public static void main(String[] args) {
        Socket socket = null;
        ServerSocket serverSocket = null;
        // 来自客户端的输入流
        BufferedReader inFromClient = null;
        // 发向客户端的输出流
        PrintWriter outForClient = null;
        try {
            // 1.0 服务端监听本地8080端口
            // 如果是本地多网卡的情况,最好手动指定一个ip,不然会随机取一个ip绑定
            serverSocket = new ServerSocket(8080);
            System.out.println("客服系统运行中...");

            // 2.0 服务端开始接受消息,没客户端连接之前一直阻塞在这里
            socket = serverSocket.accept();

            // 3.0 代码到这里,说明有客户端建立了连接,也就是TCP三次握手成功了,二者可以正常通信
            // 从socket获取输入流,用来接受客户端输入的信息
            inFromClient = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            // 从socket获取输出流,用来向客户端输出信息
            outForClient = new PrintWriter(socket.getOutputStream(), true);

            // 4.0 打印客户端输入的第一条信息,当然如果客户端没输入任何消息,服务端就会一直阻塞在这里
            System.out.println(inFromClient.readLine());
            // 收到客户端发来的第一条消息(客户端1号建立连接)后,向客户端输出一条自动回复消息
            outForClient.println("智能客服:您好!我是智能客服小V,有什么需要帮助的么");
            // 继续监听等待客户端传来的输入流
            String readline = inFromClient.readLine();

            // 5.0 只要客户不输入'bye',服务端就一直循环接受并处理客户端的输入流
            while (!"bye".equals(readline)) {
                // 模拟服务端的处理逻辑
                if (readline.contains("咨询")) {
                    outForClient.println("智能客服:需要咨询请拨打我们的热线12345");
                } else if (readline.contains("客服")) {
                    outForClient.println("智能客服:我们请不起人工客服");
                } else {
                    outForClient.println("智能客服:你的输入不合法,请重新输入");
                }
                readline = inFromClient.readLine();
            }

            // 6.0 跳出了循环,说明客户端输入了bye,服务端向客户端告别
            outForClient.println("智能客服:再见");
            System.out.println("客服系统关闭!");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭资源
                outForClient.close();
                inFromClient.close();
                serverSocket.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

    }

}

客户端如下:

package com.test.sf.socket;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;

/**
 * 客户端
 */
public class SocketClientDemo {
    public static void main(String[] args) {
        Socket socket = null;
        // 来自控制台的输入流
        BufferedReader inFromControler = new BufferedReader(new InputStreamReader(System.in));
        // 来自服务器的输入流
        BufferedReader inFromServer = null;
        // 发向服务器的输出流
        PrintWriter outForServer = null;
        try {
            // 客户端请求ip地址为127.0.0.1地址,端口为8080的应用,也就是服务端,如果服务端没启动,执行这行代码会直接报错,connect refuse
            socket = new Socket("127.0.0.1", 8080);
            inFromServer = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            outForServer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);

            // 执行到这里说明已经成功连接了服务器,通知服务器,此客户端已经连接
            // 结合服务器的逻辑,如果这行代码没执行,服务器会一直在readline那里阻塞
            outForServer.println("客户1号连接!");

            // 接收到服务器传来的欢迎消息,同理服务器没发送消息的话也会一直阻塞
            System.out.println(inFromServer.readLine());

            // 准备向服务器发送数据,数据来自控制台
            String readline = inFromControler.readLine();
            while (!"bye".equals(readline)) {
                // 只要客户端输入的不是'bye',就循环向服务端发送
                outForServer.println(readline);
                // 输出服务端发回的消息
                System.out.println(inFromServer.readLine());
                // 继续等待客户端的输入
                readline = inFromControler.readLine();
            }

            // 将bye发送给服务器
            outForServer.println(readline);
            // 打印出服务端的结束语
            System.out.println(inFromServer.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outForServer.close();
                inFromServer.close();
                inFromControler.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

执行效果图如下:
在这里插入图片描述

代码关键现象分析

  1. 注意双方通信的几个阻塞点,socket建立连接的时候会阻塞,通信时也会有IO阻塞,如果把代码换一换顺序,很容易进入死锁一样的状态,互相阻塞,至于为什么,下面分析
  2. 上面只是单线程的,也就是说再加一个客户端也没用,可以结合多线程进行思考
  3. 注意构建PringWrite输出流的时候,传了一个为true的参数,代表自动刷新缓存,否则还需要手动的out.flush().

Socket通信模型

对于TCP协议
建立连接=三次握手
开始通信=IO操作
结束通信=四次挥手
在这里插入图片描述

socket和IO阻塞

在实现上面的聊天室的时候,发现socketServer.accept()会阻塞。
在这里插入图片描述

我们用上面的聊天室代码片段、上面的结构图来做结合分析。
假设客户端对应发送端,服务端对应接收端。

  1. 服务端启动,等待客户端连接,发送数据,此时线程会阻塞
Socket socketServer = serverSocket.accept();
  1. 客户端连接服务端,服务端停止阻塞,双方的连接在此时已经建立,可以从socket中获取相关的信息。线程不会阻塞。
Socket socketClient = new Socket("127.0.0.1", 8080);
  1. 客户端发送消息给服务端
    为了使流程更清晰,我们将原本的outForServer对象构建时不做自动刷新缓存的操作
outForServer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
// "客户1号连接“这句话从客户端的用户层传到传输层,可以理解为执行了上图的send()方法
outForServer.println("客户1号连接!");
// 刷新客户端缓存区,将上述消息刷出去,也就是传出去,线程不会阻塞
outForServer.flush();
  1. 服务端接收到从客户端缓存区发来的消息,把消息存在服务端缓存区。
// 把数据从服务端缓存区读到服务端应用层,可以理解为执行了上图的recv()方法
// 在缓存区为空的时候,这个方法一直会使线程陷入阻塞,直到服务端缓存区有数据进来
String readline = inFromClient.readLine();

当然,如果接收端一直不读取缓存区的数据,数据会按顺序越来越多,直到占满缓存区,此时接收端会通知发送端,我已经塞不下了,请不要发了。
如果发送端继续发或者说还有一批数据在路上,接收端收到这些数据的时候,发现放不下,会丢弃数据,来保证可靠性

TCP滑动窗口

首先,看一下下面的动画实例
在线操作滑动窗口
看完以后,其实就隐隐明白TCP滑动窗口是什么了

预防场景

A和B通信,B处理数据能力有限,A发送的数据太快太多,超过B的处理能力,发生拥塞,所以需要一种机制,来控制发送的速度和大小。

作用

流量控制和拥塞控制

实现原理

这里回忆三次握手和四次挥手中的seq=x,ack=y这种参数

A向B发送报文时,在请求头中发送一个seq=x,表示自己发送包的首位序列号
B收到报文且接受完以后,给A发送一个回复,ACK=1,表示收到报文了,seq=x+m,表示B希望下次收到的数据包的首位序列号,ack=y表示B相应包的首位序列号。同时窗口向右滑动。
A收到B的报文后,明白B已经收到了,向右滑动m,此时一次传输已经完成
如果A再向B传输的话,会发送一个首位序列号为seq=x+m的包

在这个过程中

  1. 通过发送的确认来保证传输的速度
  2. 通过首位序列号来保证传输的大小
    从而达到流量控制的目的

功能改进-增加客户端

我们之前只有一个客户端一个服务端,现在稍作改造,增加一个客户端,向实际场景再迈进一小步
Server端改造,主要有三点:

  1. 把方法拆分,从面向过程走向面向对象
  2. 之前输入bye,客户端和服务端就都关闭了,现在做改造,输入bye只关闭客户端,以便下一个客户端使用服务端
  3. 客户端关闭,服务端与之对应的socket也需要关闭,重新监听客户端连接请求
public class SocketServerDemo {

    private Socket socket = null;
    private ServerSocket serverSocket = null;
    // 来自客户端的输入流
    private BufferedReader inFromClient = null;
    // 发向客户端的输出流
    private PrintWriter outForClient = null;

    public static void main(String[] args) {
        SocketServerDemo socketServerDemo = new SocketServerDemo();
        socketServerDemo.startServer();
    }

    SocketServerDemo(){
        // 1.0 服务端监听本地8080端口
        // 如果是本地多网卡的情况,最好手动指定一个ip,不然会随机取一个ip绑定
        try {
            serverSocket = new ServerSocket(8080);
            System.out.println("客服系统运行中...");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void startServer(){
        try {
            waitClient();
            listenClient();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                // 关闭资源
                outForClient.close();
                inFromClient.close();
                serverSocket.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void waitClient() throws IOException {

        // 2.0 服务端开始接受消息,没客户端连接之前一直阻塞在这里
        socket = serverSocket.accept();

        // 3.0 代码到这里,说明有客户端建立了连接,也就是TCP三次握手成功了,二者可以正常通信
        // 从socket获取输入流,用来接受客户端输入的信息
        inFromClient = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // 从socket获取输出流,用来向客户端输出信息
        outForClient = new PrintWriter(socket.getOutputStream(), true);

        // 4.0 打印客户端输入的第一条信息,当然如果客户端没输入任何消息,服务端就会一直阻塞在这里
        System.out.println(inFromClient.readLine());
        // 收到客户端发来的第一条消息(客户端1号建立连接)后,向客户端输出一条自动回复消息
        outForClient.println("智能客服:您好!我是智能客服小V,有什么需要帮助的么");
    }

    public void listenClient() throws IOException {
        // 监听等待客户端传来的输入流
        String readline = inFromClient.readLine();
        // 只要客户不输入'close',服务端就一直循环接受并处理客户端的输入流
        while (!"close".equals(readline)) {
            // 模拟服务端的处理逻辑
            if (readline.contains("咨询")) {
                outForClient.println("智能客服:需要咨询请拨打我们的热线12345");
            } else if (readline.contains("客服")) {
                outForClient.println("智能客服:我们请不起人工客服");
            } else if (readline.contains("bye")) {
                outForClient.println("再见!!");
                socket.close();
                this.waitClient();
            } else {
                outForClient.println("智能客服:你的输入不合法,请重新输入");
            }
            readline = inFromClient.readLine();
        }

        // 跳出了循环,说明客户端输入了close,服务端关闭
        outForClient.println("智能客服系统关闭");
        System.out.println("客服系统关闭!");
    }

}

Client端改造

  1. 拆分方法
  2. 增加一个close命令用来关闭服务端,代替原来的bye。原来的bye变成了关闭socket的命令
  3. 再复制粘贴增加另一个客户端2号
public class SocketClientDemo1 {
    private Socket socket = null;
    // 来自控制台的输入流
    private BufferedReader inFromControler = new BufferedReader(new InputStreamReader(System.in));
    // 来自服务器的输入流
    private BufferedReader inFromServer = null;
    // 发向服务器的输出流
    private PrintWriter outForServer = null;

    public static void main(String[] args) {
        SocketClientDemo1 socketClientDemo = new SocketClientDemo1("127.0.0.1", 8080);
        socketClientDemo.StartClient();
    }

    public void StartClient() {
        try {
            connectServer();
            chatWithServer();

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outForServer.close();
                inFromServer.close();
                inFromControler.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    SocketClientDemo1(String host, int port) {
        // 客户端请求ip地址为host的地址,端口为port的应用,也就是服务端,如果服务端没启动,执行这行代码会直接报错,connect refuse
        try {
            socket = new Socket(host, port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void connectServer() throws IOException {
        inFromServer = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        outForServer = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
        // 执行到这里说明已经成功连接了服务器,通知服务器,此客户端已经连接
        // 结合服务器的逻辑,如果这行代码没执行,服务器会一直在readline那里阻塞
        outForServer.println("客户1号连接!");
        // 接收到服务器传来的欢迎消息,同理服务器没发送消息的话也会一直阻塞
        System.out.println(inFromServer.readLine());
    }

    public void chatWithServer() throws IOException {

        // 准备向服务器发送数据,数据来自控制台
        String readline = inFromControler.readLine();
        while (!"bye".equals(readline) && !"close".equals(readline)) {
            // 只要客户端输入的不是'bye',就循环向服务端发送
            outForServer.println(readline);
            // 输出服务端发回的消息
            System.out.println(inFromServer.readLine());
            // 继续等待客户端的输入
            readline = inFromControler.readLine();
        }

        // 将bye发送给服务器
        outForServer.println(readline);
        // 打印出服务端的结束语
        System.out.println(inFromServer.readLine());
    }
}

运行效果如下:
在这里插入图片描述
可以看到,分为以下几步

  1. 服务端启动,等待客户端连接
  2. 客户端1启动,与服务短连接成功,开始通信。
  3. 客户端2启动,陷入等待
  4. 客户端断开连接,服务端关闭套接字,重新调用accept()
  5. 客户端2连接成功
  6. 客户端2断开连接,服务端断开连接

在这个过程中,只能有一个客户端访问服务端,其他客户端陷入阻塞,这种一次只能处理一个TCP请求的叫做迭代服务器

功能改进-增加服务端

针对上述问题,既然一个服务端一次只能服务一个客户端,那再来一个客户端请求只能阻塞,显然不太合适。
那就多创建几个服务端嘛
然而直接copy一个服务端,启动两台服务端,发现报错了
在这里插入图片描述
地址已经被使用,其实是端口被占用了
我们知道,某种程度上socket=ip+port,二者都一样的话会冲突
显然一个socket只能有一个客户端和一个服务端做连接,所以直接增加服务端也是不现实的
如果我们更改每个服务端的端口,然后结合线程池,是不是可以达到一些我们预想的效果呢?
我们后文分析
javaIO模型-Socket实现一个简单的客服聊天功能的改造(二)

标签:socket,TCP,println,new,接字,客户端,服务端,Socket
来源: https://blog.csdn.net/qq_31363843/article/details/115215111

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有