初识TCP(编写回显服务器)

打印 上一主题 下一主题

主题 945|帖子 945|积分 2835

初识TCP(编写回显服务器)

TCP相关的API

ServerSocket : 这是socket类,对应到网卡,但是这个类只能给服务器进利用用
socket : 对应到网卡,既可以给服务器使用,又可以给客户端使用
   TCP是面向字节流的,传输的基本单位是字节
  我们用一个回显服务器来演示TCP的工作过程。所谓回显服务器就是客户端发起什么请求,就返回什么相应。
服务器代码实现

  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. public class TcpEchoServer {
  11.     private ServerSocket serverSocket=null;
  12.     public TcpEchoServer(int port) throws IOException {
  13.         serverSocket= new ServerSocket(port);
  14.     }
  15.     public void start() throws IOException {
  16.         System.out.println("服务器启动!");
  17.         ExecutorService pool = Executors.newCachedThreadPool();
  18.         while (true) {
  19.             // 通过 accept 方法来 "接听电话", 然后才能进行通信
  20.             Socket clientSocket = serverSocket.accept();
  21. //            Thread t = new Thread(() -> {
  22. //                processConnection(clientSocket);
  23. //            });
  24. //            t.start();
  25.             pool.submit(new Runnable() {
  26.                 @Override
  27.                 public void run() {
  28.                     try {
  29.                         processConnection(clientSocket);
  30.                     } catch (IOException e) {
  31.                         throw new RuntimeException(e);
  32.                     }
  33.                 }
  34.             });
  35.         }
  36.     }
  37.     public void processConnection(Socket clientSocket) throws IOException {
  38.         System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
  39.         try(InputStream inputStream=clientSocket.getInputStream();
  40.             OutputStream outputStream=clientSocket.getOutputStream()
  41.         ) {
  42.             while(true){
  43.                 Scanner scanner=new Scanner(inputStream);
  44.                 if(!scanner.hasNext()){
  45.                     // 读取完毕. 客户端断开连接, 就会产生读取完毕.
  46.                     System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress(), clientSocket.getPort());
  47.                     break;
  48.                 }
  49.                 // 1. 读取请求并解析. 这里注意隐藏的约定. next 读的时候要读到空白符才会结束.
  50.                 //    因此就要求客户端发来的请求必须带有空白符结尾. 比如 \n 或者空格.
  51.                 String request=scanner.next();
  52.                 // 2. 根据请求计算响应
  53.                 String response = process(request);
  54.                 // 3. 把响应返回给客户端
  55.                 //    通过这种方式可以写回, 但是这种方式不方便给返回的响应中添加 \n
  56.                 // outputStream.write(response.getBytes(), 0, response.getBytes().length);
  57.                 //    也可以给 outputStream 套上一层, 完成更方便的写入.
  58.                 PrintWriter printWriter=new PrintWriter(outputStream);
  59.                 printWriter.println(response);
  60.                 printWriter.flush();
  61.                 System.out.printf("[%s:%d] req: %s, resp: %s\n", clientSocket.getInetAddress(), clientSocket.getPort(),
  62.                         request, response);
  63.             }
  64.         } catch (IOException e) {
  65.             throw new RuntimeException(e);
  66.         }finally {
  67.             clientSocket.close();
  68.         }
  69.     }
  70.     public String process(String request){
  71.         return request;
  72.     }
  73.     public static void main(String[] args) throws IOException {
  74.         TcpEchoServer server=new TcpEchoServer(9090);
  75.         server.start();
  76.     }
  77. }
复制代码
客户端代码实现

  1. import java.io.*;
  2. import java.net.Socket;
  3. import java.util.Scanner;
  4. public class TcpEchoClient {
  5.     private Socket socket=null;
  6.     public TcpEchoClient(String serverIp,int serverPort) throws IOException {
  7.         // 此处可以把这里的 ip 和 port 直接传给 socket 对象.
  8.         // 由于 tcp 是有连接的. 因此 socket 里面就会保存好这俩信息.
  9.         // 因此此处 TcpEchoClient 类就不必保存.
  10.         socket=new Socket(serverIp,serverPort);
  11.     }
  12.     public void start(){
  13.         System.out.println("客户端启动!!");
  14.         try(InputStream inputStream=socket.getInputStream();
  15.             OutputStream outputStream=socket.getOutputStream()
  16.         ) {
  17.             Scanner scannerConsole=new Scanner(System.in);
  18.             Scanner scannerNetwork=new Scanner(inputStream);
  19.             PrintWriter writer=new PrintWriter(outputStream);
  20.             while (true){
  21.                 // 这里的流程和 UDP 的客户端类似.
  22.                 // 1. 从控制台读取输入的字符串
  23.                 System.out.println("->");
  24.                 if(!scannerConsole.hasNext()){
  25.                     break;
  26.                 }
  27.                 String request=scannerConsole.next();
  28.                 //2.把请求发给服务器,这里需要使用println来发送,为了让发送的请求末尾带有\n
  29.                 //这里是和服务器的scanner.next呼应的
  30.                 writer.println(request);
  31.                 writer.flush();
  32.                 //3.从服务器读取响应,这里也是和服务器返回响应的逻辑对应
  33.                 String response=scannerNetwork.next();
  34.                 //4.把响应显示出来
  35.                 System.out.println(response);
  36.             }
  37.         } catch (IOException e) {
  38.             throw new RuntimeException(e);
  39.         }
  40.     }
  41.     public static void main(String[] args) throws IOException {
  42.         TcpEchoClient client=new TcpEchoClient("127.0.0.1",9090);
  43.         client.start();
  44.     }
  45. }
复制代码
部门代码解释

1.TCP是有毗连的
和打电话一样,需要客户端拨号,服务器来接听: serverSocket.accept()
   accept也是一个大概会产生阻塞的利用。如果当前没有客户端连过来,此时accept就会阻塞
  2.为何这里要设置两个socket?

想象一个场景:你要去买房
ServerSocket
把服务器想象成售楼处,ServerSocket 就是售楼处里负责等购房者上门的工作人员。先确定好办公地点(端口号),然后通过accept()等购房者来。购房者一来,就安排出一个专门的购房洽谈室(Socket),用于后续沟通交流。
Socket
Socket 就犹如那个购房洽谈室,是服务器(售楼处)和客户端(购房者)交流互动的专属空间,双方在这里收发信息,像在洽谈室里探讨买房的各种事儿一样,完成数据通信。
每个客户端都会分配一个洽谈室。
   这样的优点是:
  分工明确,支持多客户端访问
  3.TCP有毗连
TCP socket中就会保存对端的信息

4.这里 的利用相称于把字节流转换成字符流
   Java中,字节流(如InputStream和OutputStream)是以字节为单位来处理数据的。字符流(如Reader和Writer)是以字符为单位来处理数据的。
  这里的PrintWriter是继续于Writer。转换一下方便后续利用

5.断开毗连

注意事项

1.客户端输入之后,服务器没有相应

之所以出现上述的情况,本质原因在于PrintWriter内置的缓冲区在作祟。
   为什么要设置缓冲区呢?由于IO利用都是比力低效的利用,就盼望可以或许让低效利用,进行的只管少一些。
  因此引入缓冲区(内存),先把要写入网卡的数据放到内存缓冲区中,比及攒一波之后再统一进行发送(把多次IO合并成一次了)
  但是也有个问题,如果发送的数据很少,此时由于缓冲区还没满,数据就待在缓冲区里,没有被真正被发送出去
  【解决方法】手动革新缓冲区,flush革新缓冲区


2.上述代码需要进行close吗?
serverSocket不需要,socket需要
serverSocket整个程序只有唯逐一个对象,并且这个对象的生命周期很长是要跟随整个程序的,这个对象无法提前关闭,只要程序退出,随着进程的烧毁一起被开释即可(不需要手动进行)
但是TCP的client socket是每个客户端都有一个,随着客户端越来越多,这里消耗的socket也会越来越多(如果不加开释,就很大概把文件形貌符表给占满)
   【调用socket.close本质上也是关闭文件,开释文件形貌符表,这里进程烧毁,文件形貌符表就没了】
  

3.当多个客户端来同时访问服务器
两个客户端同时发起请求,服务器只给第一个客户端相应,停止第一个客户端后,服务器才会给第二个客户端相应!这是肯定不可的。
   服务器服务多个客户端是天经地义的!!
  

【问题分析】第一个客户端连上服务器之后,服务器就会从accept这里返回(排除阻塞)进入到processConnection中了。接下来就会在scanner.hasNext这里阻塞,等候客户端的请求。客户端请求到达之后,sanner.hasNext返回,继续执行,读取请求,根据请求计算相应,返回相应给客户端…执行完上述一轮利用之后,循环返来继续再hasNext阻塞,等候下一个请求。直到客户端退出之后,毗连竣事,此时循环才会退出。
虽然第二个客户端和服务器在内核层面上建立了TCP毗连了,但是应用程序这里无法把毗连拿到应用程序里处理
(人家给你打电话,你手机一直在响,但是你就是没接)
   那么问题来了:第一个客户端退出了,第二个客户端之前发的请求啥的咋就立刻被处理,而没被丢掉呢??
  这是由于当前TCP在内核中,每个socket都是有缓冲区的
  客户端发送的数据确实是发了,服务器也收到了,只不外数据是在服务器的吸收缓冲区中
  一旦第一个客户端退出了,回到第一层循环,继续执行第二次accept,继续执行next就能把之前缓冲区的内容读出来
  【解决方法】焦点思绪是使用多线程,单个线程无法既能给客户端循环提供服务,又能去快速调用到第二次accept
简单的办法就是引入多线程
   主线程就负责执行accept,每次有一个客户端连上来,就分配一个新的线程,由新的线程负责给客户端提供服务。
  


   上述问题,不是TCP引起的,而是代码没写好,两层循环嵌套引起的
  UDP服务器,只有一层循环,就不涉及到这样的问题,之前UDP服务器天然就可以处理多个客户端的请求
  4.如果客户端比力多,就会使服务器频仍创建烧毁线程
线程池就能解决频仍创建烧毁的问题
5.如果当前的场景是线程频仍创建,但是不烧毁呢
【解决方案】
1.引入协程
轻量级线程,本质上还是一个线程,用户态可以通过手动调度的方法让一个线程“并发”的做多个任务(省去系统变更的开销了)
2.IO多路复用
系统内核级别的机制,本质上是让一个线程同时去负责处理多个socket(由于这些socket数据并非时同一时刻都需要处理)
效果展示



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

小小小幸运

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表