【网络编程】理解客户端和服务器并使用Java提供的api实现回显服务器

服务器 0

目录

一、网络编程

二、客户端和服务器

三、客户端和服务器的交互模式

四、TCP 和 UDP

UDP socket api 的使用

1、DatagramSoket

2、DatagramPacket

TCP socket api 的使用

1、ServerSocket

2、Socket 



一、网络编程

本质上就是学习传输层给应用层提供的 api,通过 api 把数据交给传输层,进一步地层层封装将数据通过网卡发送出去,这也是网络程序的基本工作流程。

掌握了基础 api 就能更好的理解实际开发中使用的框架(spring,dubbo)的工作过程,也提供了魔改/自己实现框架的能力。

二、客户端和服务器

在网络中,主动发起通信的一方称为“客户端”,被动接受的一方称为“服务器”。同一个程序在不同的场景中,可能是客户端也可能是服务器。

客户端给服务器发送的数据,称为“请求”(request)

服务器给客户端返回的数据,称为“响应”(response)

三、客户端和服务器的交互模式

1、“一问一答”

一个请求对于一个响应,这是交互模式是最常见的,后续进行的“网站开发”(web开发)都是这种模式。

2、“一问多答”

主要在“下载”场景中涉及

3、“多问一答”

主要在“上传”场景中涉及

4、“多问多答”

主要在“远程控制/远程桌面”场景中涉及

四、TCP 和 UDP

进行网络编程,需要使用系统的 API,【本质上是传输层提供的协议】

传输层主要涉及到两个协议:TCP 和 UDP。

连接性可靠性面向数据传输方式
TCP面向连接可靠传输面向字节流全双工
UDP无连接不可靠传输面向数据报全双工
  • 连接:此处说的“连接”不是物理意义的连接,是抽象虚拟的“连接”。所谓计算机中的“网络连接”是指通信双方各自保存对方的信息。客户端的数据结构中记录了谁是它的服务器;服务器的数据结构中记录了谁是它的客户端;本质上就是记录对方的信息
  • 可靠传输/不可靠传输:无论如何都不能保证100%的信息传输。可靠传输主要是指发送方能够感知数据有没有传输给接收方,如果没接收到,可以采取相应的措施补救,例如重传机制。
  • 面向字节流:与文件中的字节流完全一致,网络中传输数据的基本单位就是字节
  • 面向数据报:每次传输的基本单位是一个数据报(有一系列字节构成)。
  • 全双工:一个信道,可以双向通信,就叫全双工。可以理解成马路的多车道,就是全双工。
  • 半双工:可以理解为吸管,同一时刻只能吸或者呼。

UDP socket api 的使用

Java 把系统原生 api 封装了,UDP socket 提供的两个核心的类

1、DatagramSoket

操作系统中有一类文件,就叫 socket 文件,这类文件抽象地表示了“网卡”这样的硬件设备。而进行网络通信最核心的硬件设备就是网卡。

DatagramSocket 类就是负责对 socket 文件进行读写,从而借助网卡发送接收数据。

2、DatagramPacket

UDP 面向数据报,每次发送接收数据的基本单位是一个 UDP 数据报

DatagramPacket 类就表示了一个 UDP 数据报。

关于 receive 接收数据报的底层实现过程

UdpEchoServer 实例

public class UdpEchoServer {    private DatagramSocket socket = null;    public UdpEchoServer(int port) throws SocketException {        socket = new DatagramSocket(port);    }    public void start() throws IOException {        System.out.println("服务器启动");        while (true) {            //每次循环,都是一次处理请求,进行响应的过程            //1. 读取请求并解析            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);            socket.receive(requestPacket);            // 将读到的字节数组转换成 String 方便后续操作            String request = new String(requestPacket.getData(),0,requestPacket.getLength());            //2. 根据请求计算响应            String response = process(request);            //3. 把响应返回到客户端            // 与请求数据报创建不同,请求数据报是使用空白字节数组,而此处直接把 String 里包含的字节数组作为参数创建,            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,                    requestPacket.getSocketAddress());  // 因为 UDP 无连接,因此必须从【请求数据报】中获取对应客户端的 ip 和端口            socket.send(responsePacket);            //打印日志            System.out.printf("[%s:%d] req: %s, resp: %s/n",requestPacket.getAddress().toString(),                    requestPacket.getPort(), request, response);        }    }    private String process(String request) {        return request;    }    public static void main(String[] args) throws IOException {        UdpEchoServer server = new UdpEchoServer(9090);        server.start();    }}

 上述代码中:

1、可以看到 23 行需要从【请求数据报】中获取对应客户端 ip 和端口号才能完成发送响应,证明了 UDP socket 自身不保存对端的 ip 和端口号,体现了无连接

2、不可靠传输,代码中没有体现。

3、receive 和 socket 都是以DatagramPacket 为单位,体现了面向数据报

4、一个 socket 既能发送(send)有能接收(receive),体现了全双工

UdpEchoClient 示例 

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 {        System.out.println("客户端启动");        Scanner sc = new Scanner(System.in);        while (true) {            //1. 从控制台读取要发送的数据            System.out.print("-> "); //表示提示用户输入            if (!sc.hasNext()) {   //hasNext 具有阻塞功能                break;            }            String request = sc.next();            //2. 构造请求并发送            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 response = new String(responsePacket.getData(),0,responsePacket.getLength());            System.out.println(response);        }    }    public static void main(String[] args) throws IOException {        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);        client.start();    }}

TCP socket api 的使用

由于 TCP 是面向字节流的,传输的基本单位是字节,因此没有像 UDP 中 DatagramPacket 这样的类。
Java 把系统原生 api 封装了,TCP socket 提供的两个核心的类

1、ServerSocket

这是Socket 类,同样抽象地表示了“网卡”但是这个类与 UDP 中使用的 DatagramSocket 不同,这个类只能给服务器进行使用。只负责处理对客户端的连接,主要 api 是 accept()

2、Socket 

对应到“网卡”,既能给服务器使用,又能给客户端使用。相当于电话的两个听筒,通过 Socket 完成对端之间的通信。主要的 api 是 getInputStream 和 getOutputStream
需要注意:由于服务器端的 Socket 对象与客户端时一一对应的,为了避免无限占用文件描述符表,使用完毕后需要 close 关闭。

TcpEchoServer 示例

public class TcpEchoServer {    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 后(new socket),就会和对应的服务器进行 tcp 连接建立流程            // 此时通过 accept 方法来“接听电话”,然后才能进行通信            Socket clientSocket = serverSocket.accept();            Thread t = new Thread(() -> {                processConnection(clientSocket);            });            t.start();        }    }    private void processConnection(Socket clientSocket) {        System.out.printf("[%s:%d] 客户端上线!/n", clientSocket.getInetAddress(), clientSocket.getPort());        // 循环读取客户端的请求并返回响应        try (InputStream inputStream = clientSocket.getInputStream();             OutputStream outputStream = clientSocket.getOutputStream()){            // 可以使用 inputStream 原本的 read 方法进行读取            // 但是比较繁琐,为了【方便读入】,这里使用 Scanner 对输入流进行输入            Scanner sc = new Scanner(inputStream);            while (true) {   // 长连接                if (!sc.hasNext()) {                    // 读取完毕,客户端断开连接                    System.out.printf("[%s:%d] 客户端下线!/n", clientSocket.getInetAddress(), clientSocket.getPort());                    break;                }                //1. 读取请求并解析,此处使用 next ,需要注意 next 的读入规则                String request = sc.next();                //2. 根据请求计算响应                String response = process(request);                //3. 把响应返回给客户端                /*  通过这种方式也可以写回,但是这种方式不方便添加 /n                outputStream.write(response.getBytes(),0,response.getBytes().length);*/                // 因此为了【方便写入】,给 outputStream 也套一层,即使用 printWriter                // 此处的 printWriter 就类似于 Scanner 将输入流包装了一下,而 printWriter 对输出流包装了一下                PrintWriter printWriter = new PrintWriter(outputStream);                // 通过 println 在末尾添加了 /n,与客户端的 scNetwork.next 呼应                printWriter.println(response);                // 刷新缓冲区,确保数据能够发送出去                printWriter.flush();                // 打印日志                System.out.printf("[%s:%d] req: %s, resp: %s/n", clientSocket.getInetAddress(), clientSocket.getPort(),                        request,response);            }        } catch (IOException e) {            throw new RuntimeException(e);        } finally {                clientSocket.close();        }    }     private String process(String request) {        return request;    }    public static void main(String[] args) throws IOException {        TcpEchoServer server = new TcpEchoServer(9090);        server.start();    }}

需要注意的是:
1、理解ServerSocket 和Socket 的不同作用,Socket作为接收对象。
2、只有当客户端 new Socket 时,ServerSocket 才能通过 accept 完成连接。
3、Scanner 和 PrintWriter 。
4、flush 刷新缓冲区。
5、finaly{ clientSocket.close(); } 每个客户端对应一个Socket,因此每个客户端完成任务后,需要关闭文件,从而销毁文件描述符表。而 try()自动关闭的是流对象,而没有释放文件本体。

TcpEchoClient 示例

public class TcpEchoClient {    private Socket socket = null;    public TcpEchoClient(String serverIp, int serverPort) throws IOException {        // 这里直接将 ip 和 port 传入,是由于 tcp 是有连接的,socket 里能够保存 ip 和 port        socket = new Socket(serverIp,serverPort);        // 因此也不需要额外创建【类成员对象】来保存 ip 和 port    }    public void start() {        System.out.println("客户端启动!");        try (InputStream inputStream = socket.getInputStream();             OutputStream outputStream = socket.getOutputStream()){            // 此处的 scanner 用于控制台读取数据            Scanner scConsole = new Scanner(System.in);            // 此处的 scanner 用于读取服务器响应回来的数据            Scanner scNetwork = new Scanner(inputStream);            // 此处 printWriter 用于向服务器写入请求数据            PrintWriter printWriter = new PrintWriter(outputStream);            while (true) {                // 这里流程和 UDP 的客户端类似                //1. 从控制台读取输入的字符串                System.out.print("-> ");                if (!scConsole.hasNext()) {                    break;                }                String request = scConsole.next();                //2. 把请求发送给服务器,                // 使用 printWriter 是为了使发送的请求末尾带有 /n,与服务器的 sc.next 呼应                printWriter.println(request);                // 刷新缓冲区,确保数据能够发送出去                printWriter.flush();                //3. 从服务器读取响应                String response = scNetwork.next();                //4. 打印响应                System.out.println(response);            }        } catch (IOException e) {            throw new RuntimeException(e);        }    }    public static void main(String[] args) throws IOException {        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);        client.start();    }}

需要注意的是:
1、TCP 是有连接的,因此 Socket 能够直接保存 ip 和 port。
2、flush 刷新缓冲区。

【博主推荐】

【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136288256?spm=1001.2014.3001.5501【Java多线程】对线程池的理解并模拟实现线程池-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136160003?spm=1001.2014.3001.5501【Java多线程】分析线程加锁导致的死锁问题以及解决方案-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/zzzzzhxxx/article/details/136150237?spm=1001.2014.3001.5501

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

如果觉得作者写的不错,求给博主一个大大的点赞支持一下,你们的支持是我更新的最大动力!

也许您对下面的内容还感兴趣: