实现一个TCP客户端——服务端协议

目录

TCP客户端常见的API:

ServerSocket:

Socket:

TCP服务端(单线程版本)

      属性+构造方法:

      启动服务端的start()方法

        步骤一:接收客户端发送的socket

      步骤二: 调用processConnection方法来处理客户端发送的连接

         ①通过参数传入的clientSocket来获取输入、输出流对象,此处采用的是try()的方法来关闭流对象

  ②通过scanner.next()来不断读取内容;

 ③构造response,来作出响应

 ④通过OutputStream+PrintWriter来发送response字符串给客户端

⑤关闭连接

TCP客户端(单线程版本)

      属性+构造方法

     start方法

      步骤1:通过socket来获取到与服务端进行数据交互的inputStream和OutputStream

        步骤2:从控制台获取用户输入的信息

       步骤3:把读取到的request以流的形式发送给服务端,获取响应

       步骤4:通过Scanner读取服务器的响应,并且回显

 TCP服务端(支持多个客户端发送请求)

       多线程版本服务端 

      线程池版本服务端

TCP长连接/短连接问题

      短连接的工作过程:

     长连接的工作过程:


     TCP协议的具体介绍,已经在上一篇文章当中提到了。

     同时,上一篇文章也手写了一个Udp协议。

(2条消息) 认识UDP、TCP协议_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128709206?spm=1001.2014.3001.5501      在这一篇文章当中,udp的客户端和服务端之间的通信,使用的时DatagramSocket和DatagramPacket这两个api来完成传递信息的。DatagramSocke负责发送和接收消息,DatagramPacket用来传输报文


TCP客户端常见的API:

而在TCP协议当中,提供的API主要是下面两个类:

ServerSocket:专门给服务端使用的socket。

Socket:既可以提供给客户端使用,也可以给服务端使用。

ServerSocket:

构造方法:

方法签名方法说明
ServerSocket(int port)创建一个服务端嵌套字,并且指定服务端所占用的进程

成员方法: accept

方法签名方法说明
Socket accept()

accept方法,用来表示建立客户端与服务端的连接

前面一篇文章当中,我们提到了,TCP是"有连接"的协议,TCP客户端与服务端一定要建立连接,才可以互相发送消息。因此这个accept方法,返回的socket对象,服务端就是通过这个socket对象和客户端进行通信的。

如果服务端没有收到socket对象,那么就会阻塞等待,无法进行通信。


Socket:

       对于服务端来说,是由accept()方法返回的的,返回的socket对象用于和客户端进行通信。

 构造方法 

       对于客户端来说,在客户端的构造方法当中,需要构造对象的时候,指定一个IP以及端口号

这个IP以及端口号都是服务端


  两个常用普通方法

方法签名方法说明
getInputStream()通过socket对象,获取到内部的输入流对象
getOutputStream()通过socket对象,获取到内部的输出流对象

TCP服务端(单线程版本)

      属性+构造方法:

       需要在TcpEchoServer内部封装一个属性,这个属性是ServerSocket。

       在构造方法当中,需要指定ServerSocket占用哪个端口号,此端口号就是服务端的端口号

       代码实现:

/**
 * @author 25043
 */
public class TcpEchoServer {

    /**
     * 用于Tcp客户端与服务端通信
     * 的socket对象
     */
    private ServerSocket serverSocket;



    public TcpEchoServer(int port) throws IOException {
        //指定服务端进程占用的端口号
        serverSocket=new ServerSocket(port);
    }

      启动服务端的start()方法

        步骤一:接收客户端发送的socket

  //使用clientSocket来与客户端进行交流
  Socket clientSocket=serverSocket.accept();

        此处serverSocket.accept()方法的效果是接收客户端发送的连接。

        一个客户端对应一个accept方法获取的clientSocket

        由于Socket代表一个文件,任何一个文件会对应进程当中的一个文件描述符表。

        也就是这个socket会占用额外的磁盘空间,因此当客户端和服务端通信结束之后,需要把这个连接释放掉(释放的操作,会在后面提到)


        客户端在构造socket对象的时候,就会指定服务端的IP以及端口号。

        客户端如果想与服务端通信,一定需要建立连接!!因此,如果发服务端启动之后,没有客户端发送连接过来,那么服务端就会在accept()方法这里阻塞等待。


        因此,在服务端当中,通信的逻辑应当是这样的:

        


      步骤二: 调用processConnection方法来处理客户端发送的连接

       需要注意的是:一个Socket对应的是一个客户端发送的连接,但是在processConnection内部额有可能涉及处理多个客户端连接的步骤:也就是在Tcp协议当中,服务端客户端的关联关系为一对多。

       但是,以下的代码,先来体验一下单线程的模式。最后,将会演示一个多线程版本。

         ①通过参数传入的clientSocket来获取输入、输出流对象,此处采用的是try()的方法来关闭流对象

//获取clientSocket当中输入、输出流对象
try(InputStream inputStream= clientSocket.getInputStream();


OutputStream outputStream= clientSocket.getOutputStream()) {

  以下②③④一共3个步骤,需要在while(true)循环内部不断进行,直到scanner无法读取到内容了 

  ②通过scanner.next()来不断读取内容;

//2、根据请求构造响应


//通过scanner.next()的方式来读取,需要注意的是


//scanner遇到空格/换行符/其他空白字符会停止读取


//但是,读取的结果里面不会包含这三种符号


String request=scanner.next();

 ③构造response,来作出响应

//回写的内容
String response="服务端已经响应:"+request;

 ④通过OutputStream+PrintWriter来发送response字符串给客户端

  //使用PrintWriter来发送outputStream

  PrintWriter printWriter=new PrintWriter(outputStream);

  printWriter.println(response);

  //刷新缓冲区,保证当前数据一定会被发送出去

  printWriter.flush();

⑤关闭连接

 在finally代码块当中,关闭连接,释放文件描述符表。

finally {
           try {
                //关闭此次连接
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

  整体服务端代码(单线程版)

/**
 * @author 25043
 */
public class TcpEchoServer {

    /**
     * 用于Tcp客户端与服务端通信
     * 的socket对象
     */
    private ServerSocket serverSocket;



    public TcpEchoServer(int port) throws IOException {
        //指定服务端进程占用的端口号
        serverSocket=new ServerSocket(port);
    }

    /**
     * 启动服务端
     */
    public void start() throws IOException {
        System.out.println("启动服务端");
        while (true){
            //使用clientSocket来与客户端进行交流
            Socket clientSocket=serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    /**
     * 处理客户端发送的连接
     * 客户端发送的连接@param clientSocket
     */
    private void processConnection(Socket clientSocket) {
        //输出客户端的IP以及端口号
        System.out.println("客户端已经上线!客户端的IP是:"
                +clientSocket.getInetAddress()+
                ";客户端的端口是:"+clientSocket.getPort());
        //获取clientSocket当中输入、输出流对象
        try(InputStream inputStream= clientSocket.getInputStream();
            OutputStream outputStream= clientSocket.getOutputStream()) {
            //使用while循环,处理多个请求+响应
            while (true){
                //1、通过scanner来读取inputStream
                Scanner scanner=new Scanner(inputStream);
                //读取完毕之后,直接返回:
                if(!scanner.hasNext()){
                    System.out.println("客户端已经下线!客户端的IP是:"
                            +clientSocket.getInetAddress()+
                            ";客户端的端口是:"+clientSocket.getPort());
                    //退出循环
                    break;
                }
                //2、根据请求构造响应
                //通过scanner.next()的方式来读取,需要注意的是
                //scanner遇到空格/换行符/其他空白字符会停止读取
                //但是,读取的结果里面不会包含这三种符号
                String request=scanner.next();
                //构造回写的内容response
                String response="服务端已经响应:"+request;
                //使用PrintWriter来发送outputStream
                PrintWriter printWriter=new PrintWriter(outputStream);
                printWriter.println(response);
                //刷新缓冲区,保证当前数据一定会被发送出去
                printWriter.flush();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

TCP客户端(单线程版本)

      属性+构造方法

        此处,需要一个socket,指定服务端的IP+端口号

 private Socket socket;

 public TcpEchoClient(String serverIp,int port) throws IOException {
    //指定服务端的ip+端口号
    socket=new Socket(serverIp,port);
 }

       如果客户端想和服务端进行通信,就一定需要指定服务端的端口号。因为TCP是有连接的协议,不允许在没有建立连接的情况下面发送消息。

       当socket对象被创建之后,也就意味着客户端成功与服务端建立连接。

       客户都安的socket创建之后的一瞬间,服务端的accept方法已经接收早到就客户端的socket对象。


     start方法

      步骤1:通过socket来获取到与服务端进行数据交互的inputStream和OutputStream

       需要注意的是,从客户端的socket获取的InputStream和OutputStream都是相对于客户端来进行输入/输出操作的。


        步骤2:从控制台获取用户输入的信息

   //1.客户端从键盘上面读取内容
   String request=input.next();

       步骤3:把读取到的request以流的形式发送给服务端,获取响应

                //2.把读取到的内容构造成请求,发送到客户端
                PrintWriter printWriter=new PrintWriter(outputStream);
                printWriter.println(request);
                //加上flush,刷新缓冲区
                printWriter.flush();

      可以看到,此处,使用的是printWriter.println(request)来发送字符串的

      但是,是否可以替换成print,也就是不采用\n呢?

      答案是,不可以:原因:

      在服务端当中,是使用Scanner scanner=input.next()来接收客户端发送的内容的:

     

       回顾一下scanner.next()在什么时候会停止读取,那就是在读取到\n或者空格或者空白字符的时候,就会停止读取。因此,此处客户端发送的内容当中,一定要带有\n,才可以确scanner.next()停止读取。 


步骤4:通过Scanner读取服务器的响应,并且回显

                //读取服务器响应
                Scanner scanner=new Scanner(inputStream);
                String response= scanner.next();
                //把响应的内容回显到界面上面
                System.out.println(response);

 整体客户端代码:

/**
 * Tcp客户端
 * @author 25043
 */
public class TcpEchoClient {
    private Socket socket;

    public TcpEchoClient(String serverIp,int port) throws IOException {
        //指定服务端的ip+端口号
        System.out.println("服务端已经指定端口号"+System.currentTimeMillis());
        socket=new Socket(serverIp,port);

    }
    public void start(){
        System.out.println("客户端启动!");
        Scanner input=new Scanner(System.in);
        //此处获取到的输入流、输出流对象,都是已经跟客户端建立了联系的
        try (InputStream inputStream= socket.getInputStream();
             OutputStream outputStream= socket.getOutputStream()){
            while (true){
                //1.客户端从键盘上面读取内容
                String request=input.next();
                //2.把读取到的内容构造成请求,发送到客户端
                PrintWriter printWriter=new PrintWriter(outputStream);


                printWriter.println(request);
                //加上flush
                printWriter.flush();
                //读取服务器响应
                Scanner scanner=new Scanner(inputStream);
                String response= scanner.next();
                //把响应的内容回显到界面上面
                System.out.println(response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        //指定服务端的ip以及端口号
        TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
        tcpEchoClient.start();
    }

}

单线程TCP存在问题分析:

      TCP的服务端的核心代码就是start方法

       当服务端启动之后,如果有客户端与服务端建立联系,那么accept方法就会返回一个socket对象,服务端使用这个socket对象与客户端进行通信。

       紧接着,服务端在processConnection方法当中,针对客户端发送过来的clientSocket进行不断地使用while循环,调用scanner.next方法进行读取操作。


       那么,也就意味着,只要客户端不下线,服务端就会一直停留在这个processConnection的while循环当中。

       由于在上述的代码当中,服务端的代码是单线程的。因此,服务端无法从processConnection方法当中离开,即使其他的客户端想再次给服务端建立连接,服务端也accept不到。


      但是,如果在processConnection当中不采用while循环,那么这样可以吗?

      也是不行的,原因:

         如果不使用while循环,那么,服务端只会读取一次客户端发送的请求,也就是调用一次scanner.next方法,然后服务端就会把连接给close掉了

        那么这个客户端如果想再次建立连接,就需要重新获取连接,也就是再次new一个Socket对象。

        但是,在上面客户端的代码当中,调用客户端构造方法的时候,只创建了一个连接,也就是一个socket。

         因此,如果客户端想要再次发送消息,就没有办法发送了。

           客户端代码: 

           


 TCP服务端(支持多个客户端发送请求)

       多线程版本服务端 

        大部分的代码写法都和单线程的一致,唯一的区别就在于,每调用一次processConnection方法需要创建新一个线程来执行。

        这样,每获取到一个clientSocket,就会创建一个新的线程t来执行processConnection方法。

        即使线程t出现了异常情况,无法结束运行,也不会影响主线程不断接收新的客户端连接。

        代码实现:

    /**
     * 启动服务端(多线程版)
     */
    public void start() throws IOException {
        System.out.println("启动服务端");
        while (true){
            //使用clientSocket来与客户端进行交流
            Socket clientSocket=serverSocket.accept();
            Thread t=new Thread(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
            t.start();
        }
    }

      线程池版本服务端

       以上代码,在客户端数量不大的情况下面,是可以行得通的

       但是,如果客户端数量比较庞大,并且线程的创建销毁工作也是开销比较大的,因此,可以考虑使用线程池来处理processConnection方法,这样就可以减少了线程不断创建、销毁带来的开销。

        代码实现:

 private ExecutorService threadPool= Executors.newCachedThreadPool();
    /**
     * 启动服务端(多线程版)
     */
    public void start() throws IOException {
        System.out.println("启动服务端");
        while (true){
            //使用clientSocket来与客户端进行交流
            Socket clientSocket=serverSocket.accept();
            //往线程池当中提交任务
            threadPool.submit(() -> processConnection(clientSocket));
        }
    }

TCP长连接/短连接问题

      短连接的工作过程:

       客户端与服务器建立连接

       发送一次请求

       读取响应

       关闭连接

       下次通信,就需要再一次建立连接。可以看到,短连接每一次通信只会建立一次连接


     长连接的工作过程:

        ①客户端与服务端建立连接

        ②客户端发送消息

        ③读取响应

        ④根据需求,尝试再次发送消息(也就是回到2)

        ⑤重复之间若干次,再决定是否断开连接


       可以看到,长连接的特点就是一次连接多次发送消息。而短连接,就是一次连接只可以发送一次请求。看似长连接的复用性更高,但是其实也不一定说要使用长连接的策略才好,需要结合具体的应用场景。 

  • 11
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值