• 【Java网络编程】 三


    本文主要介绍了TCP版本的回显服务器的编写。

    一.TCP版本回显服务器

    1.服务器

    服务器的实现流程

    1.接收请求并解析

    2.根据请求计算出响应(业务流程

    3.把响应返回给客户端

    代码:

    1. import java.io.IOException;
    2. import java.io.InputStream;
    3. import java.io.OutputStream;
    4. import java.io.PrintWriter;
    5. import java.net.ServerSocket;
    6. import java.net.Socket
    7. import java.util.Scanner;
    8. import java.util.concurrent.ExecutorService;
    9. import java.util.concurrent.Executors;
    10. /**
    11. * Tcp版本的回显服务器
    12. *
    13. * 服务器
    14. */
    15. public class TcpEchoServer {
    16. private ServerSocket serverSocket=null;
    17. //使用线程池:此处不应该创建固定线程数目的线程池
    18. private ExecutorService service= Executors.newCachedThreadPool();
    19. public TcpEchoServer(int port) throws IOException {
    20. serverSocket=new ServerSocket(port);
    21. }
    22. //这个操作会绑定端口
    23. public void start() throws IOException {
    24. System.out.println("服务器启动");
    25. while(true){
    26. //从内核中的连接获取到应用程序中
    27. /**
    28. *
    29. * accept是把内核中已经建立好的连接,给拿到应用程序中,但是这里的返回值并非是
    30. * 一个connection对象,而只是一个socket对象,这个socket对象就像一个耳麦
    31. * 可以说话,也可以听到对方的声音
    32. */
    33. Socket clientSocket=serverSocket.accept();
    34. //单个线程,不方便完成这里的一边拉客,一边介绍;就需要多线程
    35. //多线程负责拉客
    36. //每次有一个新的客户端,都创建一个新的线程去服务
    37. // Thread t=new Thread(()->{
    38. // try {
    39. // processConnection(clientSocket);
    40. // } catch (IOException e) {
    41. // e.printStackTrace();
    42. // }
    43. //
    44. // });
    45. // t.start();
    46. //使用线程池也可以解决
    47. service.submit(new Runnable() {
    48. @Override
    49. public void run() {
    50. try {
    51. processConnection(clientSocket);
    52. } catch (IOException e) {
    53. e.printStackTrace();
    54. }
    55. }
    56. });
    57. }
    58. }
    59. //通过这个方法来处理一个连接的逻辑
    60. private void processConnection(Socket clientSocket) throws IOException {
    61. System.out.printf("[%s:%d]客户端上线 \n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
    62. //接下来就可以读取请求,根据请求计算响应,返回响应三步走
    63. /**
    64. * socket对象内部包含了两个字节流对象,可以把指责两个对象获取到
    65. * 完成后续的读写工作
    66. */
    67. try(InputStream inputStream=clientSocket.getInputStream();
    68. OutputStream outputStream=clientSocket.getOutputStream()){
    69. while(true){
    70. //1.根据请求并解析,为了读取方便,直接使用scanner
    71. Scanner scanner=new Scanner(inputStream);
    72. if(!scanner.hasNext()){
    73. //读取完毕,客户端下线
    74. System.out.printf("[%s:%d]客户端下线 \n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
    75. break;
    76. }
    77. /**
    78. *这里暗含了一个约定,客户端发过来的请求
    79. * 得是文本数据,同时还要包含空白符
    80. */
    81. String request=scanner.next();
    82. //next一直读到空白符结束(换行,回车,空格,制表符,等)
    83. //2.根据请求计算响应
    84. String response=process(request);
    85. //3.把响应写给客户端
    86. /**
    87. 用printWriter把outputstream包裹一下,方便进行收发数据
    88. */
    89. PrintWriter writer=new PrintWriter(outputStream);
    90. /**
    91. * 使用printWriter的println方法,把响应写给客户端,结尾\n,
    92. * 是为了方便客户端读取响应,使用scanner.next读取
    93. */
    94. writer.println(response);
    95. /**
    96. * 还需要加一个刷新缓冲区操作
    97. * io操作比较有开销,相比于访问内存,进行io次数越多,程序的速度就越慢
    98. *
    99. * 作为一块内存作为缓冲区,写数据的时候,先写到缓冲区里
    100. * 存一波数据,统一进行io
    101. * printwriter内置了缓冲区
    102. * 手动刷新,确保这里的数据是真的通过网卡发出去了,而不是残留在缓冲区里
    103. *
    104. * 加上flush是更稳妥的做法。
    105. */
    106. writer.flush();
    107. //打印日志
    108. System.out.printf("[%s:%d] rep:%s , resp:%s \n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
    109. request,request);
    110. }
    111. } catch (IOException e) {
    112. e.printStackTrace();
    113. }finally {
    114. /**
    115. * socek有很多,每来一个连接,就会有一个连接
    116. */
    117. //finally中加上close操作,确保当前socket及时关闭。
    118. clientSocket.close();
    119. }
    120. }
    121. public String process(String request){
    122. return request;
    123. }
    124. public static void main(String[] args) throws IOException {
    125. TcpEchoServer server=new TcpEchoServer(9090);
    126. server.start();
    127. }
    128. }

    说明

    1.循环之后,服务器要做的事情不是读取客户端的请求,而是先处理客户端的连接,因为TCP是面向连接的。

    2.一个服务器中,要对应很对客户端,服务器内核中有很多客户端连接。虽然内核中连接很多,但是应用程序还是要一个一个的处理。

    我们可以把内核中的连接看成 待办事项, 待办事项在队列中,应用程序需要一个一个完成这些任务

    要完成任务,就要先取任务 ; 因此在处理请求之前,要先通过accept()从内核中获得请求


    我们可以把TCP连接的生成和获得连接的过程看作一个生产者消费者模型。

    socket中会包含一个管理连接的队列,这个队列是每个socket都有一份,相互之间不会混淆。


    3.当服务器执行到accept时,此时如果客户端还没来,accept就会阻塞,直到有客户端连接成功为止。

    accept是把内核中已经建立好的连接,拿到应用程序中,返回值是一个socket对象,这个对象就像一个耳麦,既可以说话,也可以听到对反的声音。

    也就是通过socket对象就可以和对方进行网络通信


    此时这个回显服务器中,涉及到两种socket

    1.ServerSocket

    相当于是在店外揽客的服务员,揽到客人之后,交给店内的服务员

    2.clientSocket

    店内负责招待的服务员

    4.

    scanner和printwriter没有close,并不会导致文件资源暴露

    流对象中持有的资源的两个部分

    1)内存(对象销毁,内存回收)

    2)   文件描述符  scanner和printwriter持有的是inputstream和outpustream的引用

    5.服务器怎么感知到客户端下线的

    hasNext()在客户端没有发请求的时候,也会阻塞,一直阻塞到客户端发了请求,或者是客户端退出,它就返回了

    2.客户端

    基本实现流程:

    1.从控制台读取用户的输入

    2.把输入的内容构造成请求发送给服务器

    3.从服务器读取响应

    4.把响应显示到控制台上

    代码:

    1. import java.io.IOException;
    2. import java.io.InputStream;
    3. import java.io.OutputStream;
    4. import java.io.PrintWriter;
    5. import java.net.Socket;
    6. import java.util.Scanner;
    7. /**
    8. * Tcp版本的服务器
    9. *
    10. * 客户端
    11. */
    12. public class TcpEchoClient {
    13. private Socket socket=null;
    14. //要和服务器通信,就需要先知道,服务器所在的位置
    15. public TcpEchoClient(String serverIp,int serverPort) throws IOException {
    16. //这个new操作就完成了tcp连接的建立
    17. socket = new Socket(serverIp, serverPort);
    18. }
    19. private void start() {
    20. System.out.println("客户端启动");
    21. Scanner scannerConsole=new Scanner(System.in);
    22. try(InputStream inputStream=socket.getInputStream();
    23. OutputStream outputStream=socket.getOutputStream()){
    24. while(true){
    25. //1.从控制台输入字符串
    26. System.out.print("->");
    27. String request=scannerConsole.next();
    28. //2.把请求发送给服务器
    29. PrintWriter printWriter=new PrintWriter(outputStream);
    30. printWriter.println(request);
    31. /**
    32. * 不要忘记flush
    33. * 确保数据真的发送出去了
    34. */
    35. printWriter.flush();
    36. //3.从服务器读取响应
    37. Scanner scannerNetwork=new Scanner(inputStream);
    38. String response=scannerNetwork.next();
    39. //4.把响应打印出来
    40. System.out.println(response);
    41. }
    42. } catch (IOException e) {
    43. e.printStackTrace();
    44. }
    45. }
    46. public static void main(String[] args) throws IOException {
    47. TcpEchoClient client=new TcpEchoClient("127.0.0.1",9090);
    48. client.start();
    49. }
    50. }

    二.问题和解决方法

    1.服务器问题

    1.关闭当前的socket!!放在finally当中

    客户端会有很多,而每个客户端都有一个socket,如果不关闭会消耗大量的资源。

    2.(重点!上面的代码是修改后的!)

    两个以上(包含)客户端发来的请求,服务器无法正确地处理。

    这是因为当第一个客户端来了,accept会返回,进入processConnection

    在处理这个客户端请求过程中,即使第二个客户端来了,也无法第二次调用accept

    解决办法:改进成多线程

    主线程:负责accept,和客户端建立连接

    然后创建新的线程,让新的线程去处理客户端的各种请求

    更好的办法:使用线程池!

    这样可以避免频繁创建和销毁线程。

  • 相关阅读:
    [CG] 用 Docker 配置 Ubuntu OpenGL 环境
    MybatisPlus整合SpringBoot
    Spring-MVC的文件上传下载,及插件的使用(让项目开发更节省时间)
    回归预测 | Matlab实现GWO-ESN基于灰狼算法优化回声状态网络的多输入单输出回归预测
    牛客刷题——前端面试【三】谈一谈Promise、封装ajax、json数据使用
    Linux学习笔记——进程管理
    云原生之持续交付
    Day19:C++STL迭代器/Lambda表达式/仿函数/函数适配器和包装器
    Python内置函数--iter()&next()
    16:00面试,16:06就出来了,问的问题有点变态。。。
  • 原文地址:https://blog.csdn.net/weixin_63210321/article/details/134020300