当前位置:   article > 正文

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

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占用哪个端口号,此端口号就是服务端的端口号

       代码实现:

  1. /**
  2. * @author 25043
  3. */
  4. public class TcpEchoServer {
  5. /**
  6. * 用于Tcp客户端与服务端通信
  7. * 的socket对象
  8. */
  9. private ServerSocket serverSocket;
  10. public TcpEchoServer(int port) throws IOException {
  11. //指定服务端进程占用的端口号
  12. serverSocket=new ServerSocket(port);
  13. }

      启动服务端的start()方法

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

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

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

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

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

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


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

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


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

        


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

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

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

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

  1. //获取clientSocket当中输入、输出流对象
  2. try(InputStream inputStream= clientSocket.getInputStream();
  3. OutputStream outputStream= clientSocket.getOutputStream()) {

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

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

  1. //2、根据请求构造响应
  2. //通过scanner.next()的方式来读取,需要注意的是
  3. //scanner遇到空格/换行符/其他空白字符会停止读取
  4. //但是,读取的结果里面不会包含这三种符号
  5. String request=scanner.next();

 ③构造response,来作出响应

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

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

  1. //使用PrintWriter来发送outputStream
  2. PrintWriter printWriter=new PrintWriter(outputStream);
  3. printWriter.println(response);
  4. //刷新缓冲区,保证当前数据一定会被发送出去
  5. printWriter.flush();

⑤关闭连接

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

  1. finally {
  2. try {
  3. //关闭此次连接
  4. clientSocket.close();
  5. } catch (IOException e) {
  6. e.printStackTrace();
  7. }
  8. }

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

  1. /**
  2. * @author 25043
  3. */
  4. public class TcpEchoServer {
  5. /**
  6. * 用于Tcp客户端与服务端通信
  7. * 的socket对象
  8. */
  9. private ServerSocket serverSocket;
  10. public TcpEchoServer(int port) throws IOException {
  11. //指定服务端进程占用的端口号
  12. serverSocket=new ServerSocket(port);
  13. }
  14. /**
  15. * 启动服务端
  16. */
  17. public void start() throws IOException {
  18. System.out.println("启动服务端");
  19. while (true){
  20. //使用clientSocket来与客户端进行交流
  21. Socket clientSocket=serverSocket.accept();
  22. processConnection(clientSocket);
  23. }
  24. }
  25. /**
  26. * 处理客户端发送的连接
  27. * 客户端发送的连接@param clientSocket
  28. */
  29. private void processConnection(Socket clientSocket) {
  30. //输出客户端的IP以及端口号
  31. System.out.println("客户端已经上线!客户端的IP是:"
  32. +clientSocket.getInetAddress()+
  33. ";客户端的端口是:"+clientSocket.getPort());
  34. //获取clientSocket当中输入、输出流对象
  35. try(InputStream inputStream= clientSocket.getInputStream();
  36. OutputStream outputStream= clientSocket.getOutputStream()) {
  37. //使用while循环,处理多个请求+响应
  38. while (true){
  39. //1、通过scanner来读取inputStream
  40. Scanner scanner=new Scanner(inputStream);
  41. //读取完毕之后,直接返回:
  42. if(!scanner.hasNext()){
  43. System.out.println("客户端已经下线!客户端的IP是:"
  44. +clientSocket.getInetAddress()+
  45. ";客户端的端口是:"+clientSocket.getPort());
  46. //退出循环
  47. break;
  48. }
  49. //2、根据请求构造响应
  50. //通过scanner.next()的方式来读取,需要注意的是
  51. //scanner遇到空格/换行符/其他空白字符会停止读取
  52. //但是,读取的结果里面不会包含这三种符号
  53. String request=scanner.next();
  54. //构造回写的内容response
  55. String response="服务端已经响应:"+request;
  56. //使用PrintWriter来发送outputStream
  57. PrintWriter printWriter=new PrintWriter(outputStream);
  58. printWriter.println(response);
  59. //刷新缓冲区,保证当前数据一定会被发送出去
  60. printWriter.flush();
  61. }
  62. } catch (IOException e) {
  63. e.printStackTrace();
  64. }finally {
  65. try {
  66. clientSocket.close();
  67. } catch (IOException e) {
  68. e.printStackTrace();
  69. }
  70. }
  71. }
  72. }

TCP客户端(单线程版本)

      属性+构造方法

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

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

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

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

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


     start方法

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

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


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

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

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

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

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

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

      答案是,不可以:原因:

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

     

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


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

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

 整体客户端代码:

  1. /**
  2. * Tcp客户端
  3. * @author 25043
  4. */
  5. public class TcpEchoClient {
  6. private Socket socket;
  7. public TcpEchoClient(String serverIp,int port) throws IOException {
  8. //指定服务端的ip+端口号
  9. System.out.println("服务端已经指定端口号"+System.currentTimeMillis());
  10. socket=new Socket(serverIp,port);
  11. }
  12. public void start(){
  13. System.out.println("客户端启动!");
  14. Scanner input=new Scanner(System.in);
  15. //此处获取到的输入流、输出流对象,都是已经跟客户端建立了联系的
  16. try (InputStream inputStream= socket.getInputStream();
  17. OutputStream outputStream= socket.getOutputStream()){
  18. while (true){
  19. //1.客户端从键盘上面读取内容
  20. String request=input.next();
  21. //2.把读取到的内容构造成请求,发送到客户端
  22. PrintWriter printWriter=new PrintWriter(outputStream);
  23. printWriter.println(request);
  24. //加上flush
  25. printWriter.flush();
  26. //读取服务器响应
  27. Scanner scanner=new Scanner(inputStream);
  28. String response= scanner.next();
  29. //把响应的内容回显到界面上面
  30. System.out.println(response);
  31. }
  32. } catch (IOException e) {
  33. e.printStackTrace();
  34. }
  35. }
  36. public static void main(String[] args) throws IOException {
  37. //指定服务端的ip以及端口号
  38. TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",9090);
  39. tcpEchoClient.start();
  40. }
  41. }

单线程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出现了异常情况,无法结束运行,也不会影响主线程不断接收新的客户端连接。

        代码实现:

  1. /**
  2. * 启动服务端(多线程版)
  3. */
  4. public void start() throws IOException {
  5. System.out.println("启动服务端");
  6. while (true){
  7. //使用clientSocket来与客户端进行交流
  8. Socket clientSocket=serverSocket.accept();
  9. Thread t=new Thread(new Runnable() {
  10. @Override
  11. public void run() {
  12. processConnection(clientSocket);
  13. }
  14. });
  15. t.start();
  16. }
  17. }

      线程池版本服务端

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

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

        代码实现:

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

TCP长连接/短连接问题

      短连接的工作过程:

       客户端与服务器建立连接

       发送一次请求

       读取响应

       关闭连接

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


     长连接的工作过程:

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

        ②客户端发送消息

        ③读取响应

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

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


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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/158779
推荐阅读
相关标签
  

闽ICP备14008679号