JavaEE 初阶 -- 网络原理初识 & 网络编程套接字

初识网络

网络发展背景

单机阶段 -> 局域网阶段 -> 广域网阶段 -> 移动互联网阶段

局域网

  • 基于网线连接:用几根网线把几台主机连接在一起
  • 基于集线器组建:集线器就是相当于把一根网线的一头劈成了两半,但是单两半都有数据传过来的时候,就会打架
  • 基于交换机组建:一个交换机上面会有很多网口,用几根网线就能让主机和交换机组成一个局域网了
  • 基于交换机和路由器组建

广域网

一个国家的网络,就可以认为是广域网

IP

描述了在网络上主机的位置
IP地址是一个32位的二进制数,通常用点分十进制的方式来表示。
如:127.0.0.1是一个环回IP地址,主要用于本机到本机的通信方式。

端口号

描述了一个主机上的应用程序
两个不同的进程,不能绑定同一个端口号,但是一个进程可以绑定多个端口号

协议

在网络通信中,需要约定的协议,其实是非常复杂的,面对复杂的环境,就需要复杂的协议,但是协议太复杂了也不好。一个协议太复杂,就可以拆分为多个协议,协议是拆分很多,存在有些小的协议,作用或者定位是类似的,就可以针对这些小协议,进行分类,同时再这些不同的类别,进行分层,分层就是相当于约定了层级与层级之间的调用关系,要求上层协议调用下层协议,下层协议给上层提供支持,不能跨层调用。

在网络通信的时候,本质上传输的是光信号与电信号,通过光信号的频率(高低频)和电信号的电平(高低电平)来表示 0 / 1,这一串 0 / 1 就需要通信双方约定好协议。

协议分层的好处

拿打电话举个例子,你和你朋友打电话(此时用的是座机),你们约定的协议就是普通话,座机之间有座机协议。协议就分成了两层,后续就可以比较容易的针对这里的某一层协议进行替换。比如把座机改为了用手机打电话,或者是我们用英语进行通话。

  1. 分层之后就可以做到,层次之间,耦合程度比较低,上层协议不必了解下层的细节,下层也不必了解上层的细节
  2. 方便对某一层的协议进行替换

网络协议的分层

  • OSI 七层网络模型(仅存在于教科书上的)
  • TCP / IP 五层网络模型(当下最广泛使用的模型)

在这里插入图片描述
在这里插入图片描述

  • 传输层还有个OSPF(开放的最短路径优先协议)

在协议分层背景下,数据如何通过网络传输?

封装 & 分用

发送方发送数据,要把数据从上到下,依次交给对应的层次的协议,进行封装
接收方接收数据,要把数据从下到上,依次交给对应的层次的协议,进行解析

以qq发消息为例子:这里面的封装本质上就是字符串的拼接

  • 发送过程:
  1. 应用层(qq应用程序)拿到上述用户数据,进行封装,封装成应用层数据包,一个典型的数据报都是通过报头 + 载荷的方式构成的,比如会封装进去时间,A的qq号,B的qq号等,当然,为了区分上述字段,可能会引入分隔符或者长度信息来做界定
  2. 传输层拿到上述数据,应用层要调用传输层提供的api,来处理这个数据,传输层有很多协议,最典型的就是TCP和UDP,以UDP为例,UDP针对上述的数据包再进行封装,UDP数据报头会封装进去源端口和目的端口
  3. 传输层到网络层,把这个数据报交给网络层,网络层,最主要的就是IP协议,网络层给数据报加上一个IP协议的报头,包含源 IP 和目的 IP ,这两个IP就是描述了这次传输最初的起点和最终的终点
  4. 网络层交给数据链路层,最典型的协议叫做以太网,封装一个以太网帧头和一个以太网帧尾,包含源mac和目的mac,mac地址,也叫物理地址,也是描述一个主机在网络上的位置,它的功能和IP很相似,但是当下就是把两个地址作用于两个不同的用途,IP用来进行网络层的路径规划,mac用来进行描述数据链路层两个即将进行传输的相邻节点,mac是和网卡绑定的,每个网卡,每个设备都会有自己唯一一个mac地址(理论上),但是IP则不是
  5. 数据链路层就是把上述以太网数据帧交给物理层了,物理层要把上述 0 / 1 的二进制数据转化为光信号/电信号/电磁波信号,进行传输了。
    数据链路层完成帧同步,差错控制,流量管理,链路管理
    上述过程操作系统帮我们封装好了
  • 接收过程:
  1. 物理层,网卡收到高低电平二进制数据,就会对这里的信号继续解析,还原成 0 / 1 这样的二进制序列
  2. 从物理层交给数据链路层,此时就把上述 0 / 1 这系列数据当作一个以太网数据帧(此处就是从以太网线,收到的数据,就要交给以太网协议来处理了),把帧头帧尾去掉,取出中间的载荷,再往上交给网络层,以太网数据帧头中有一个消息类型,根据这个类型就知道了网络层是IP协议了
  3. 来到网络层,此时就由网络层的IP协议进行解析数据报,也是去掉IP报头,同时会做一些工作,最重要的还是取出载荷,交给上层的传输层协议,IP数据报头也有一个字段,标识当前传输层用的是哪个协议
  4. 来到传输层,此处是由UDP来解析处理,还是去掉报头,取出载荷,把数据交给应用层,传输层借助端口号来区分具体的应用程序,每个要接受网络数据的程序都需要关联上一个端口号
  5. 来到应用,由qq这个程序,进行解析应用层数据报,取出下列字段,放到程序的界面中,qq服务器需要根据接收者的qq号找到对应的客户端在哪个机器上登录

真实的网络环境中,数据的传输中间可能要经历很多节点进行转发,比如中间有个交换机:

交换机会进行分用,从物理层分用到数据链路层,知道当前这个数据报的目的 mac 对不对就行了,然后交换机针对这个数据重新封装,从数据链路层封装到物理层,把数据继续转发。重新封装源 mac 和目的 mac 就变了。

如果中间有个路由器:

路由器收到的数据,会从物理层分用到网络层,根据当前得到的目的 IP 进行下一阶段的寻路操作( IP 协议是在一边传输的过程中,一边规划路径的)。之后把数据包重新进行封装,从网络层封装到物理层(此时经过数据链路层的时候,也会调整 mac 地址)

所以说:中间的交换机,只需要封装分用到数据链路层(只需要改源mac和目的mac)
中间的路由器,则需要封装分用到网络层(也需要改mac,同时还需要根据目的IP进行下一阶段的路径规划)。通常也说,交换机进行二层转发,路由器进行三层转发。

  • 有人问,群聊是怎么传输的呢?

在这里插入图片描述

Socket 套接字

网络编程套接字,是由系统提供用于网络通信的技术,也是由操作系统给应用程序提供的一组API,是基于TCP / IP 协议的网络通信的基本操作单元。socket 可以视为是应用层和传输层的桥梁,应用层和传输层之间,交互数据就是靠的socket API

操作系统给我们提供的API,主要是两组:基于UDP的API 和TCP 的 API ,TCP 和 UDP 协议差别很大,所以这两组 API 的差别也很大:
在这里插入图片描述

  • 有无连接:

比如发短信,就相当于无连接通信,直接投递,不需要接受连接,就能通信
打电话,就是有连接通信,需要先把连接接受了,才能通信

  • 可靠传输与不可靠传输

可靠传输:发送方知道接收方有没有接收到消息,但是不是对方能100%接收到,假如你要发出去的时候,网线被人剪断了,你就知道这数据没有成功传输出去了
不可靠传输:把短信发出去了,你想发给谁就输入他的手机号,发出去就发出去了,不会知道对方有没有看到或者是手机号有没有改。

  • 面向字节流和面向数据报

面向字节流:数据是以字节为单位,进行传输的,就是说假设有100个字节的数据,可以一次发完,也可以一次发10个字节,发送10次,也可以一次发20个字节,发5次
面向数据报:以数据报为单位进行传输,每个数据报都会明确大小,一次发送/接收必须是一个完整的数据报。

  • 半双工和全双工

一根管子就类似于半双工
高速公路就相当于全双工

UDP数据报套接字编程

主要涉及两个类:DatagramSocket 和 DatagramPacket

  • DatagramSocket 中 Socket 表示的是:这是一个DatagramSocket 对象,就对应到操作系统中的一个socket文件,这个socket文件,就是对应着网卡这种硬件设备的,从socket文件中读取数据,本质上就是读取网卡,从socket文件中写入数据,本质上就是写入网卡,所以我们就需要一个socket对象来进行网络编程,socket就相当于是个遥控器。
  • DatagramPacket 这个类,就代表了一个UDP数据报,使用UDP传输数据的基本单位,每次发送 / 接收数据,都是在传输一个DatagramPacket对象。

DatagramSocket API

DatagramSocket 是UDP Socket,用于发送和接收UDP数据报
在这里插入图片描述

此处的socket对象可能被客户端 / 服务器都使用的,服务器这边的socket往往要关联一个具体的端口号(必须要不变),客户端这边则不需要手动指定,系统自动分配一个空闲的端口号(则不要求)。比如把服务器看成是要开一家店,你在宣传你的店的时候肯定需要把地址给写上吧。

在这里插入图片描述

socket 也是文件,文件用完了记得关闭,否则会出现文件资源泄露的问题

DatagramPacket API

DatagramPacket是UDP Socket发送和接收的数据报
DatagramPacket 构造方法:

方法签名方法说明
DatagramPacket(byte[]buf, int length)构造一个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)
DatagramPacket(byte[]buf, int offset, int length,SocketAddress address)构造一个DatagramPacket以用来发送数报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号

第一个版本,不需要设置地址进去,通常用来接受消息
第二个版本,需要显式的设置地址进去,通常要用来发送消息

在这里插入图片描述

回显服务 – UDP 版本

下面基于UDP socket 写一个客户端服务器的回显服务(客户端发了请求,服务器返回一个一模一样的响应)

一个服务器,主要要做3个核心工作:

  1. 读取请求并解析
  2. 根据请求计算响应(省略了)
  3. 把响应返回到客户端

数据咋在socket存储(了解即可)
在这里插入图片描述

服务器程序的代码:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    // 需要先定义一个 socket 对象
    // 通过网络通信,必须要使用 socket 对象
    private DatagramSocket socket = null;

    // 绑定一个端口,不一定能成功
    public UdpEchoServer(int port) throws SocketException {
        // 构造 socket 的同时,指定要关联/绑定的端口
        socket = new DatagramSocket(port);
    }

    // 启动服务器的主逻辑
    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true) {
            // 每次循环,要做三件事:
            // 1. 读取请求并解析
            //    构造空饭盒
            DatagramPacket requesPacket = new DatagramPacket(new byte[4096], 4096);
            //    食堂大妈给饭盒里盛饭(饭从网卡上来)
            socket.receive(requesPacket);
            //    为了方便处理这个请求,把数据转成 String
            String request = new String(requesPacket.getData(), 0, requesPacket.getLength());
            // 2. 根据请求计算响应(此处省略这个步骤)
            String response = process(request);
            // 3. 把响应的结果写回到客户端
            //    根据 response 字符串,构造一个DatagramPacket
            //    和请求 packet 不同,此处构造响应的时候,需要指定这个包要给发给谁
            //        requestPacket 是从客户端这里收来的,getSocketAddress 就会得到客户端的ip和端口
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, requesPacket.getSocketAddress());
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req: %s, resp: %s\n", requesPacket.getAddress().toString(), requesPacket.getPort(), request, response);
        }
    }

    // 这个方法希望是根据请求计算响应.
    // 由于咱们写的是个 回显 程序. 请求是啥, 响应就是啥!!
    // 如果后续写个别的服务器, 不再回显了, 而是有具体的业务了, 就可以修改 process 方法,
    // 根据需要来重新构造响应.
    // 之所以单独列成一个方法, 就是想让同学们知道, 这是一个服务器中的关键环节!!!
    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
        udpEchoServer.start();
    }
}

在这里插入图片描述

客户端程序的代码:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    // 客户端启动, 需要知道服务器在哪里!!
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        // 对于客户端来说, 不需要显示关联端口.
        // 不代表没有端口, 而是系统自动分配了个空闲的端口.
        socket = new DatagramSocket();
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        // 通过这个客户端可以多次和服务器进行交互.
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 先从控制台, 读取一个字符串过来
            //    先打印一个提示符, 提示用户要输入内容
            System.out.print("-> ");
            String request = scanner.next();
            // 2. 把字符串构造成 UDP packet, 并进行发送.
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIP), serverPort);
            socket.send(requestPacket);
            // 3. 客户端尝试读取服务器返回的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            // 4. 把响应数据转换成 String 显示出来.
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.printf("req: %s, resp: %s\n", request, response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
        udpEchoClient.start();
    }
}

在这里插入图片描述
服务器和客户端交互效果:
在这里插入图片描述
小结:
在这里插入图片描述
其实最后还得调用 close 方法来关闭文件的,但是这个程序文件的生命周期退出 while 循环之后,main 紧接着就结束了,意味着进程也结束了,所有的文件资源也就自动释放了。

字典服务
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDictServer extends UdpEchoServer {
    private Map<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);

        dict.put("dog", "小狗");
        dict.put("cat", "小猫");
        dict.put("fuck", "卧槽");
    }

    @Override
    public String process(String d) {
        return dict.getOrDefault(d, "未找到改单词");
    }

    public static void main(String[] args) throws IOException {
        UdpDictServer udpDictServer = new UdpDictServer(9090);
        udpDictServer.start();
    }
}

在这里插入图片描述

TCP流套接字编程

TCP 也是两个核心的类

ServerSocket API

ServerSocket 是创建TCP服务端Socket的API

ServerSocket 构造方法:

方法方法说明
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口

在这里插入图片描述
在这里插入图片描述

Socket API

在这里插入图片描述
Socket 方法:

方法签名方法说明
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流

在这里插入图片描述

TCP的长短连接

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
对比以上长短连接,两者区别如下:

  • 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时
    的,长连接效率更高。
  • 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
  • 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
回显服务 – TCP 版本
服务器程序的代码:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoServer {
    // serverSocket 就是外场拉客的小哥
    // clientSocket 就是内场服务的小姐姐.
    // serverSocket 只有一个. clientSocket 会给每个客户端都分配一个~
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while(true) {
            Socket clientSocket = serverSocket.accept(); // 这里的 clientSocket 是服务器的 Socket
            processConnection(clientSocket);
        }
    }

    // 通过这个方法来处理一个连接.
    // 读取请求
    // 根据请求计算响应
    // 把响应返回给客户端
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // try () 这种写法, ( ) 中允许写多个流对象. 使用 ; 来分割
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            // 没有这个 scanner 和 printWriter, 完全可以!! 但是代价就是得一个字节一个字节扣, 找到哪个是请求的结束标记 \n
            // 不是不能做, 而是代码比较麻烦.
            // 为了简单, 把字节流包装秤了更方便的字符流~~
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true) {
                // 1. 读取请求
                if(!scanner.hasNext()) {
                    // 读取的流到了结尾了 (对端关闭了)
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 直接使用 scanner 读取一段字符串.
                String request = scanner.next();
                // 2. 根据请求计算响应
                String response = process(request);
                // 3. 把响应写回给客户端. 不要忘了, 响应里也是要带上换行的.
                printWriter.println(response);
                printWriter.flush(); // 立即刷新缓冲区,确保服务器第一时间感知到请求
                System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}

在这里插入图片描述

客户端程序的代码:
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int port) throws IOException {
        // 这个操作相当于让客户端和服务器建立 tcp 连接.
        // 这里的连接连上了, 服务器的 accept 就会返回.
        socket = new Socket(serverIp, port); // 这里的 new 就是在与服务器建立连接
    }

    public void start() {
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner scannerFromSocket = new Scanner(inputStream);
            while(true) {
                // 1. 从键盘上读取用户输入的内容.
                System.out.print("-> ");
                String request = scanner.next();
                // 2. 把读取的内容构造成请求, 发送给服务器.
                //    注意, 这里的发送, 是带有换行的!!
                printWriter.println(request);
                printWriter.flush(); // 立即刷新缓冲区,确保服务器第一时间感知到请求
                // 3. 从服务器读取响应内容
                String response = scannerFromSocket.next();
                // 4. 把响应结果显示到控制台上.
                System.out.printf("req: %s; resp: %s\n", request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

交互过程:
在这里插入图片描述
但是,如果没有printWriter.flush(),当我们程序运行起来,在客户端那里输入内容后,发现服务器却没有反应,因为:
在这里插入图片描述
在这里插入图片描述
所以 printWriter.println(request); 这里只是把数据写入到内存的缓冲区中,等到缓冲区满了,才会真正写网卡,但是我们可以手动刷新缓冲区,让数据立即被写入网卡,就是使用 printWriter.flush(),当然了读取也是有缓冲区的,有两个缓冲区,接收缓冲区和发送缓冲区。

  • 细节

当前,程序已经跑起来了,已经可以正常通信了,但是还有一个非常严重的bug,服务器需要同时能够给多个客户端提供服务的,但是当我们启动两次客户端的时候,发现只有第一个客户端能和服务器通信,第二个没有反应:
在这里插入图片描述
改进后客户端的代码:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class TcpEchoServer {
    // serverSocket 就是外场拉客的小哥
    // clientSocket 就是内场服务的小姐姐.
    // serverSocket 只有一个. clientSocket 会给每个客户端都分配一个~
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while(true) {
            Socket clientSocket = serverSocket.accept(); // 这里的 clientSocket 是服务器的 Socket
//            Thread t = new Thread(() -> {
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    e.printStackTrace();
//                }
//            });
//            t.start();
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    // 通过这个方法来处理一个连接.
    // 读取请求
    // 根据请求计算响应
    // 把响应返回给客户端
    private void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // try () 这种写法, ( ) 中允许写多个流对象. 使用 ; 来分割
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            // 没有这个 scanner 和 printWriter, 完全可以!! 但是代价就是得一个字节一个字节扣, 找到哪个是请求的结束标记 \n
            // 不是不能做, 而是代码比较麻烦.
            // 为了简单, 把字节流包装秤了更方便的字符流~~
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true) {
                // 1. 读取请求
                if(!scanner.hasNext()) {
                    // 读取的流到了结尾了 (对端关闭了)
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 直接使用 scanner 读取一段字符串.
                String request = scanner.next();
                // 2. 根据请求计算响应
                String response = process(request);
                // 3. 把响应写回给客户端. 不要忘了, 响应里也是要带上换行的.
                printWriter.println(response);
                printWriter.flush(); // 立即刷新缓冲区,确保服务器第一时间感知到请求
                System.out.printf("[%s:%d] req: %s; resp: %s\n", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            clientSocket.close();
        }
    }

    private String process(String request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
        tcpEchoServer.start();
    }
}

那么,有个问题:为什么UDP版本的程序在没有使用多线程的情况,也能执行多个客户端的请求?

因为UDP不需要处理连接,所以只需要一个循环,就能处理所有客户的请求了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值