飞道的博客

TCP实现回显服务器及客户端

358人阅读  评论(0)

目录

前言:

Socket API

SeverSocket API

TCP中的长短连接

TCP实现回显服务器

代码实现(有详细解释)

TCP实现回显客户端

代码实现(有详细注释)

小结:


前言:

    上篇文章介绍了TCP的特点。由于TCP的特点是有连接,面向字节流,可靠传输等,我们就可以想象到TCP的代码和UDP会有一定的差异。TCP和UDP具体使用哪种协议需要根据实际业务需求来选择。

Socket API

    Socket是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。

    不管是客户端还是服务端Socket,都是双方建立连接后,保存两端信息,及用来与对方收发数据的。

Socket构造方法

注意:

    创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接。当服务端accept()阻塞时,客户端一旦实例出Socket对象,就会建立连接。

Socket方法 

注意:

    获得套接字输入流。如果建立连接,服务端调用这个方法,就是读取客户端请求。

注意:

    获得套接字输出流。如果建立连接,服务端调用这个方法,就是往客户端返回响应。

注意:

    连接后获得对方的IP地址。

SeverSocket API

    ServerSocket 是创建TCP服务端Socket的API。

ServerSocket构造方法

    创建服务端套接字,并绑定端口。这个对象就是用来与客户端建立连接的。

ServerSocket方法

注意:

    开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,用来收发数据,否则阻塞等待。

 

注意:

    由于在操做系统中Socket被当作文件处理,那么就需要释放PCB中文件描述符表中的资源,同时断开连接。

TCP中的长短连接

    短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
    长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。

注意:

    1)建立关闭连接耗时:很明显短连接需要不断的建立和断开连接,而长连接只需要一次。长连接耗时要比短连接短。

    2)主动发送请求不同:短连接一般是客户端主动向服务端发送请求。长连接客户端可以向服务端主动发送,服务端也可以主动向客户端发送。

    3)两者使用场景不同:短连接一般适用于客户端请求频率不高的场景(浏览网页)。长连接一般适用于客户端与服务端通信频繁的场景。(聊天室)

TCP实现回显服务器

    首先服务器是被动的一方,我们必须指定端口。然后通过ServerSocket对象中accept()方法建立连接,当返回Socket对象时,处理连接并且将响应写回客户端。

    由于不知道客户端什么时候建立连接,那么服务器就需要一直等待(随时待命)。这里使用了死循环的方式,但是不会一直循环,accept()方法当没有连接时就会阻塞等待。

    这里是本机到本机的数据发送,即使用环回ip即可。


  
  1. private ServerSocket serverSocket = null;
  2. public TcpEchoSever (int port) throws IOException {
  3. serverSocket = new ServerSocket(port);
  4. }

注意:

   创建ServerSocket对象,并且指定端口号。

Socket clintSocket = serverSocket.accept();

注意:

    accept()方法会阻塞等待。客户端Socket对象一旦实例化,就会与服务端建立连接。

 processConnection(clintSocket);

注意:

    这里通过一个方法来处理连接。这样写会有很大的好处。


  
  1. try( InputStream inputStream = clintSocket.getInputStream();
  2. OutputStream outputStream = clintSocket.getOutputStream())

注意:

    我们首先需要获得读和写的流对象。服务器需要接收请求(读),返回响应(写)。这里使用的是带有资源的try(),这样就会自动关闭流对象。


  
  1. Scanner scanner = new Scanner(inputStream);
  2. String request = scanner.next();

注意:

    这里通过Scanner去从流对象中读取数据。注意这里的next()方法,当读到一个换行符/空格/其他空白符结束,但最终结果不包含上述空白符。

    因为我们不清楚客户端连接后发送多少次请求,因此我们采用死循环的方式读和向客户端响应数据。这里不会一直循环因为scanner当读不到数据就会阻塞。


  
  1. String response = process(request);
  2. public String process (String request) {
  3. return request;
  4. }

注意:

    这里通过一个函数来处理请求并且返回处理后结果。由于是回显服务器直接返回即可。


  
  1. PrintWriter printWriter = new PrintWriter(outputStream);
  2. printWriter.println(response);
  3. printWriter.flush();

注意:

    我们为了方便直接写字符串,将outputStream转换成PrintWriter。然后将响应写入到网卡,并且换行。因为客户端和服务端读数据都是需要空白符结束的,所以这里必须有一个空白符。

    由于数据首先会写入缓冲区,我们将缓冲区刷新一下保证数据正常写入到文件中(网卡)


  
  1. finally {
  2. clintSocket.close();
  3. }

注意:

    和一个客户端建立连接后,返回Socket对象(使用文件描述表),如果并发量大(会创建很多对象,文件描述符表就有可能满),就可能导致无法创建连接。因此需要保证资源得到释放,包裹在finally里。

特别注意:

    上述代码只能处理一个客户端。当代码执行到processConnection函数里,首先是一个死循环,然后还有scanner的阻塞,当处理一个连接代码就会一直在这个函数里。没有办法执行到accept()和客户端连接。想要处理下一个客户端的连接,就必须断开这个客户端,显然这是不合理的。

解决方案:

    使用多线程。当有客户端连接后,创建一个线程去处理这个连接,主线程代码继续执行,就会到accept()方法。要是有多个客户端都可以建立连接,并且有独立的线程去处理这些连接,这些线程是并发的关系。

    但是存在一个问题,如果并发量足够大(客户端数量非常多),就会创建大量的线程,也会存在大量线程的销毁,这些就会消耗大量的系统资源。因此使用线程池,使用动态变化的线程数量,根据并发量来调整线程数量。而且直接使用线程池中的线程代码上就可以实现,这样就会减少系统资源的消耗。

代码实现(有详细解释)


  
  1. public class TcpEchoSever {
  2. //Tcp协议服务器,使用ServerSocket类,来建立连接
  3. private ServerSocket serverSocket = null;
  4. public TcpEchoSever (int port) throws IOException {
  5. serverSocket = new ServerSocket(port);
  6. }
  7. public void start () throws IOException {
  8. System.out.println( "启动服务器");
  9. //使用线程池,防止客户端数量过多,创建销毁大量线程开销太大
  10. //动态变化的线程池
  11. ExecutorService threadPool = Executors.newCachedThreadPool();
  12. while ( true) {
  13. //这里会阻塞,直到和客户端建立连接,返回Socket对象,来和客户端通信
  14. //客户端构造Socket对象时,会指定IP和端口,就会建立连接(客户端主动连接)
  15. Socket clintSocket = serverSocket.accept();
  16. threadPool.submit(() -> {
  17. try {
  18. processConnection(clintSocket);
  19. } catch (IOException e) {
  20. throw new RuntimeException(e);
  21. }
  22. });
  23. //要连接多个客户端,需要多线程去处理连接
  24. //这样才能让主线程继续执行到accept阻塞,然后和其他客户端建立连接(每个线程是独立的执行流,彼此之间是并发的关系)
  25. //如果客户端数量非常大,这里就会创建很多线程,数量过多对于系统来说也是很大的开销(使用线程池)
  26. // Thread t = new Thread(() -> {
  27. // try {
  28. // processConnection(clintSocket);
  29. // } catch (IOException e) {
  30. // e.printStackTrace();
  31. // }
  32. // });
  33. // t.start();
  34. }
  35. }
  36. private void processConnection (Socket clintSocket) throws IOException {
  37. System.out.printf( "【%s : %d】客户端上线\n", clintSocket.getInetAddress(), clintSocket.getPort());
  38. //读客户端请求
  39. //处理请求
  40. //将结果写回客户端(响应)
  41. try( InputStream inputStream = clintSocket.getInputStream();
  42. OutputStream outputStream = clintSocket.getOutputStream()) {
  43. //流式数据,循环读取
  44. while ( true) {
  45. Scanner scanner = new Scanner(inputStream);
  46. //读取完毕,客户端下线
  47. if(!scanner.hasNext()) {
  48. System.out.printf( "【%s : %d】客户端下线\n", clintSocket.getInetAddress(), clintSocket.getPort());
  49. break;
  50. }
  51. //读取请求
  52. // 注意!! 此处使用 next 是一直读取到换行符/空格/其他空白符结束, 但是最终返回结果里不包含上述 空白符 .
  53. String request = scanner.next();
  54. //处理请求
  55. String response = process(request);
  56. //写回客户端处理请求结果(响应)
  57. //为了直接写字符串,这里将字节流转换为字符流
  58. //也可以将字符串转为字节数组
  59. PrintWriter printWriter = new PrintWriter(outputStream);
  60. //写入且换行
  61. printWriter.println(response);
  62. //写入首先是写入了缓冲区,这里为了保险就刷新一下缓冲区
  63. printWriter.flush();
  64. System.out.printf( "【%s : %d】请求:%s 响应:%s\n", clintSocket.getInetAddress(), clintSocket.getPort(),
  65. request, response);
  66. }
  67. } catch (IOException e) {
  68. e.printStackTrace();
  69. } finally {
  70. //和一个客户端建立连接后,返回Socket对象(使用文件描述表),如果并发量大(会创建很多对象,文件描述符表就有可能满),就可能导致无法创建连接
  71. //因此需要保证资源得到释放,包裹在finally里
  72. clintSocket.close();
  73. }
  74. }
  75. public String process (String request) {
  76. return request;
  77. }
  78. public static void main (String[] args) throws IOException {
  79. TcpEchoSever tcpEchoSever = new TcpEchoSever( 8280);
  80. tcpEchoSever.start();
  81. }
  82. }

TCP实现回显客户端

    客户端不需要指定端口号。客户端程序在用户主机上,我们如果指定就有可能和其他程序冲突,因此让操作系统随机分配一个空闲的端口号。客户端需要明确服务端的ip和端口号,这样才能明确哪个主机和哪个进程。

    那么服务端为什么可以指定端口号呢?难道就不怕和其他进程端口号冲突吗?(这里详解请看上篇文章的解释)

    首先需要明确客户端的工作流程:接收用户输入数据 --> 发送请求 --> 接收响应


  
  1. public TcpEchoClint (String severIp, int severPort) throws IOException {
  2. socket = new Socket(severIp, severPort);
  3. }

注意:

    创建Socket对象,并且指定服务端的ip和端口。当这个对象实例创建完成时,同时也就和服务端建立了连接,通过这个Socket对象就可以发送和接收数据。

    这里不需要将字符串ip进行转换,可以自动转换。


  
  1. try( InputStream inputStream = socket.getInputStream();
  2. OutputStream outputStream = socket.getOutputStream())

注意:

    和服务端一样首先获得输入和输出流。用包含资源的try可以自动关闭,释放文件描述符表中的资源。


  
  1. System.out.println( "请输入请求:");
  2. String request = scanner.next();
  3. if(request.equals( "exit")) {
  4. System.out.println( "bye bye");
  5. break;
  6. }

注意:

    让用户从控制台输入数据,这里做了一个判断,如果输入“exit”就退出客户端(break直接跳出循环)。


  
  1. PrintWriter printWriter = new PrintWriter(outputStream);
  2. printWriter.println(request);
  3. printWriter.flush();

注意:

    为了直接发送字符串,这里将outputStream转换成PrintWriter。这里在发送时需要换行(空白符),因为服务端读取的next()方法需要空白符。

    数据首先写入缓冲区,为了保证数据写入到文件(网卡),这里手动刷新一下缓冲区。


  
  1. Scanner scanner1 = new Scanner(inputStream);
  2. String response = scanner1.next();
  3. System.out.println(response);

注意:

    接收响应,通过输入流来读取响应。将接收的响应打印出来。这里的next()方法和上面一致。

代码实现(有详细注释)


  
  1. public class TcpEchoClint {
  2. Socket socket = null;
  3. public TcpEchoClint (String severIp, int severPort) throws IOException {
  4. //Socket构造方法,可以识别点分十进制,不需要转换,比DatageamPacket方便
  5. //实例这个对象的同时,就会进行连接
  6. socket = new Socket(severIp, severPort);
  7. }
  8. public void start () {
  9. try( InputStream inputStream = socket.getInputStream();
  10. OutputStream outputStream = socket.getOutputStream()) {
  11. Scanner scanner = new Scanner(System.in);
  12. while ( true) {
  13. //从控制台读取请求
  14. //空白字符结束,但不会读空白字符
  15. System.out.println( "请输入请求:");
  16. String request = scanner.next();
  17. if(request.equals( "exit")) {
  18. System.out.println( "bye bye");
  19. break;
  20. }
  21. //发送请求
  22. PrintWriter printWriter = new PrintWriter(outputStream);
  23. //需要发送空白符,因为scanner需要空白符
  24. printWriter.println(request);
  25. printWriter.flush();
  26. //接收响应
  27. Scanner scanner1 = new Scanner(inputStream);
  28. String response = scanner1.next();
  29. System.out.println(response);
  30. }
  31. } catch (IOException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. public static void main (String[] args) throws IOException {
  36. TcpEchoClint tcpEchoClint = new TcpEchoClint( "127.0.0.1", 8280);
  37. tcpEchoClint.start();
  38. }
  39. }

小结:

    在写服务端代码时,需要考虑高并发的情况。我们需要尽可能节省系统资源的利用。


转载:https://blog.csdn.net/weixin_62353436/article/details/128828445
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场