计算机网络 - 网络通信 (TCP与UDP)


前言

本篇通过了解套接字,TCP与UDP协议的特点,使用UDP的api套接字与TCP的api套接字进行回显程序的网络通信,如有错误,请在评论区指正,让我们一起交流,共同进步!


文章目录

    • 前言
    • 1. 认识网络通信需要的套接字 - socket
    • 2. UDP的api
      • 2.1 DatagramSocket
      • 2.2 DatagramPacket
      • 2.3 根据DatagramSocket 与 DatagramPacket 模拟网络通信
      • ==1.模拟实现UDP回显服务器==
      • ==2.模拟实现客户端==
    • 3. TCP的api
      • 3.1 ServerSocet
      • 3.2 Socket
      • 3.3 模拟实现TCP回显程序
      • 3.3.2 模拟实现客户端
    • 总结

本文开始

1. 认识网络通信需要的套接字 - socket

1.1 认识套接字
套接字:应用层调用传输层,操作系统内核给应用层提供一组用来网络编程的api,这就称为 socket api = 套接字api;

针对TCP与UDP协议,这里主要认识关于UDP的api 与 TCP的api 来用于网络通信;
认识api先认识 TCP 与 UDP 的特点吧!

1.2 TCP 与 UDP的主要特点
UDP:

① 无连接:使用UDP通信,双方不需要保存对端(客户端与服务器)的信息;
例如:发短信,不需要记录对方的电话,对方也不需要记录你的电话 (不用建立连接),直接根据电话号直接发送信息;
② 不可靠传输:一方发送消息,不需要知道另一方是否接收到了信息;
③ 面向数据报:以一个UDP数据报为基本单位读写数据;- 有限制;
④ 全双工:一条路径双向通信;
【注】有无连接:通信双方是否记录对方的信息;
有连接:通信双方需要记录对方的信息;
无连接:通信双方不需要记录对方的信息;

TCP:

① 有连接:使用TCP通信,双方需要保存对端的信息;
例如:打电话,一方打电话,另一方需要接通电话 (建立连接),双方才能通信;
② 可靠传输:一方发送信息,尽量保证信息发送到了另一方(但也不能保证一定成功,只是尽全力);
③ 面向字节流:以一个字节为传输基本单位 - 读写数据比较灵活
④ 全双工:双向通信;
【注】半双工:在一条路径上,A方向B方发送消息,B只能等待A发完消息后才能向A发送信息;
全双工:A向B发送消息的同时,B也可以向A发送消息;

2. UDP的api

认识socket对象:
socket 是系统中一个特殊的文件进行网络通信,需要socket文件对象,通过socket文件对象,间接操作网卡;

2.1 DatagramSocket

Datagram: 数据报;Socket: socket对象;

计算机网络 - 网络通信 (TCP与UDP)_第1张图片
构造方法:
计算机网络 - 网络通信 (TCP与UDP)_第2张图片

【注】socket对象可以被客户端 与 服务器使用;服务器使用socket需要指定关联一个端口号 - 端口号不变才能方便客户端找到服务器;客户端使用socket不需要手动指定,系统会自动分配空闲的端口号;

2.2 DatagramPacket

DatagramPacket: udp数据报对象;

计算机网络 - 网络通信 (TCP与UDP)_第3张图片
构造方法
计算机网络 - 网络通信 (TCP与UDP)_第4张图片

2.3 根据DatagramSocket 与 DatagramPacket 模拟网络通信

回显程序:自己给自己发送并接收信息;

1.模拟实现UDP回显服务器

服务器的核心工作:
① 读取客户端请求并解析
② 根据请求计算响应
③ 把响应写回到客户端

首先构造socket对象,从而间接操作网卡进行读取;
再构造带有一个参数的构造方法,给服务器分配端口;

	private DatagramSocket socket = null;
    //绑定端口号不一定会成功;- 端口号可能被别的进程占用
    //同一主机,同一时刻,一个端口只能被一个进程所占用
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

服务器主要逻辑过程:
① 读取客户端请求:使用receive()方法, 需要构造requestPacket数据报把数据读取到数据报中;
② 知道客户端输入的是字符,为方便操作将数据报(字节数组)构造为字符串;
③ 根据请求计算具体响应 - 根据具体情况修改;
④ 将响应发送回客户端,根据响应是字符串,构造数据报此时需要指定客户端的IP和端口;

public void start() throws IOException {
        System.out.println("服务器启动!");
        //服务器执行不可能只执行一个请求,需要执行多个请求就用到了循环
        while (true) {
            //1.读取客户端请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[5000],5000);
            //receive时输出型参数:在其中传入空的packet对象,然后receive方法内部就会把参数packet填充;
            // -》从网卡读取内容,写的数据报中;
            socket.receive(requestPacket);//服务器先启动,如果此时没有客户端请求,就会阻塞等待,等待客户端发送数据过来;
            //解析 -> 此处为了方便后续操作,拿字节数组构造成字符串
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //getLength : 获取真实长度;
            //2.根据请求计算响应
            String response = process(request);
            //3.把响应写回客户端 - 写回网卡
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes(StandardCharsets.UTF_8).length,
                    //requestPacket是客户端得到的,getSocketAddress得到的就是客户端的IP和端口
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印日志
            System.out.printf("[%s : %d] request: %s, response: %s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }
    //回显服务器,写什么,就返回什么;
    // 以后有其他服务器,可以根据具体请求重新构造响应;
    private String process(String request) {
        return request;
    }

【注】receive时输出型参数:在其中传入空的packet对象,然后receive方法内部就会把参数packet填充;

服务器总代码:

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[5000],5000);
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());
            //2.根据请求计算响应
            String response = process(request);
            //3.把响应写回客户端 - 写回网卡
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes(StandardCharsets.UTF_8).length,
            requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //4.打印
            System.out.printf("[%s : %d] request: %s, response: %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 udpEchoServer = new UdpEchoServer(9090);
        udpEchoServer.start();
    }
}

2.模拟实现客户端

首先构造socket对象,方便操作读取网卡;
再构造带两个参数的构造方法:指定服务器的IP和端口,让客户端容易找到服务器;

public class UdpEchoClient {
	private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    //客户端启动需要知道服务器的位置,根据服务器的IP和端口
    public UdpEchoClient(String serverIP, int serverPort) throws SocketException {
        //客户端不用指定端口,会自动分配空闲端口;
        socket = new DatagramSocket();
        //为了方便之后使用
        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }
}

客户端执行逻辑:
① 等待客户端输入请求;
② 把请求发送给服务器,需构造数据报并指定服务器IP和端口;
③ 客户端接收响应,构造数据报使用receive()接收;
④ 打印响应结果,再将数据报转换为字符串
代码如下:

     public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        //多次交互
        while (true) {
            System.out.print(" -> ");
            //1.等待控制台输入
            String request = scanner.next();
            //2.把请求发送到服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
            //通过静态发送InetAddress设置IP地址;        
            InetAddress.getByName(serverIP),serverPort);
            socket.send(requestPacket);
            //3.客户端接收响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[5000],5000);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            //4.打印日志
            System.out.printf("request: %s, response: %s\n", request,response);
        }
    }

【注】构造数据报DatagramPacket requestPacket:请求的起始0位置和请求的最后长度length; 需要服务器的IP和端口;
区别:SocketAddress:包含IP和端口;InetSocketAddress设置 IP 再单独设置端口;

客户端总代码:

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;
    //客户端启动需要知道服务器的位置,根据服务器的IP和端口
    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) {
            System.out.print(" -> ");
            //1.等待控制台输入
            String request = scanner.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[5000],5000);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());
            //4.打印日志
            System.out.printf("request: %s, response: %s\n", request,response);
        }
    }

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

客户端与服务器的网络通信过程:
① 服务器先启动,执行到receive方法后就进行阻塞,等待客户端传输数据 / 请求;
② 客户端运行,等待控制台输入数据并读取,包装成数据报后发送send() 给服务器;
③ 客户端发送数据报后,继续向下执行,到receive方法会阻塞等待服务器返回响应;
     在客户端发送请求后的后的同时,服务器会读取客户端的请求,根据请求生成响应,把响应包装成数据报再次send发送给客户端,并打印日志;
④客户端收到服务器的响应,就会解除receive的阻塞,把响应转换为字符串进行打印;
      此时服务器会进入下一轮循环,再次等待新的客户端请求;
⑤ 客户端打印完,进入下一轮,再次等待控制台输入新的数据请求;
【注】本机IP地址:127.0.0.1; - 自己与自己发信息;

3. TCP的api

3.1 ServerSocet

ServerSocket: 服务器使用的
ServerSocket构造时需要让服务器绑定一个指定的端口,方便客户端能够找到;

计算机网络 - 网络通信 (TCP与UDP)_第5张图片
构造方法:
计算机网络 - 网络通信 (TCP与UDP)_第6张图片

3.2 Socket

Socket: 服务器 和 客户端都可以使用

计算机网络 - 网络通信 (TCP与UDP)_第7张图片
构造方法
计算机网络 - 网络通信 (TCP与UDP)_第8张图片

3.3 模拟实现TCP回显程序

3.3.1 模拟实现回显服务器
了解InputStream, OutputStream操作字节流对象;
InputStream: 读数据,相当于从网卡读数据;
OutputSteam: 写数据,相当于从网卡发送数据;

首先类TcpEchoServer, 类中创建ServerSocket变量,方便以后操作网卡读取数据;
构造一个参数的构造方法,构造serverSocket对象并指定端口号;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;
    public TcpEchoServer(int port) throws IOException {
    	//与之前的UDP的DatagramSocket中的socket一样,需要绑定端口
        serverSocket = new ServerSocket(port);
    }
}

然后服务器中的主逻辑:
① 先调用serverSocket的accept方法,等待与客户端建立连接

计算机网络 - 网络通信 (TCP与UDP)_第9张图片

② 建立连接后,执行客户端连接的方法processConnectin();

核心逻辑代码:

public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            //监听,看绑定的端口是否有客户端连接,如果连接accept就会马上返回Socket对象
            Socket clientSocket = serverSocket.accept();
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    processConnection(clientSocket);
                }
            });
            t.start();
        }

为什么使用线程呢?
服务器启动后,如果不使用线程,当一个客户端占用processConnection(clientSocket)方法,有其他客户端想要再次使用processConnection(clientSocket)方法,就需要一直等待;使用多线程,来一个客户端就创建一个新线程来执行processConnection(clientSocket)方法,就可以多线程使用了;

优化为线程池:
线程的频繁创建和销毁也会带来资源销毁,这里可以优化为线程池;将线程放入线程池,减少线程的创建和销毁;

public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            //监听,看绑定的端口是否有客户端连接,如果连接accept就会马上返回Socket对象
            Socket clientSocket = serverSocket.accept();
           executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

processConnection(clientSocket)方法具体执行内容:
代码逻辑:
① 读取数据需要对网卡的操作,就用到了InputStream,OutputStream; - try() 中允许写多个流对象;
② 上述字节流,不能清楚读取到哪是一个请求;这里约定服务器是字符串请求,请求之间用\n分隔,所以包装为字符流Scanner , PrintWrite 方便读写数据; - 根据具体情况可更改;
③ 执行客户端请求,与UDP相似,但这里需要判断客户端是否断开连接使用hasNext()方法;
【注】 根据请求计算完响应,发送响应时也应该带上 \n 让客户端分清楚请求 - 使用println加 \n ;(客户端也一样)
clientSocket 是文件,不用了需要关闭;而ServerSocket不需要,它的生命周期长;(循环创建clientSocket,有一个客户端就创建一个,为防止文件资源泄露,需要close()关闭一下文件;)

代码如下:

private void processConnection(Socket clientSocket) {
        System.out.printf("客户端上线![%s : %d]\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
        try(InputStream inputStrem = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            //为了后续操作,字节流包装为字符流 - 可根据具体情况修改;
            Scanner scanner = new Scanner(inputStrem);
            PrintWriter printWriter = new PrintWriter(outputStream);
            //执行具体请求
            while (true) {
                //1.读取客户端请求
                //判断客户端是否关闭了
                if(!scanner.hasNext()) {
                    System.out.printf("客户端下线![%s : %d]\n",clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                //scanner.next读取换行结束,读取一段请求;
                String request = scanner.next();
              // 读到空白符结束;空白符:\n,空格,制表符;
                //2.根据请求做出响应
                String response = process(request);
                //3.返回响应,响应中加上\n,上述读取一段请求时next不读取\n,这里发送时需要加上;
                printWriter.println(response);
                printWriter.flush();//刷新,将缓冲区数据立即发送
                //4.打印日志
                System.out.printf("[%s : %d] request: %s, response: %s\n",clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(), request, response);

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
                clientSocket.close();
        }
    }

为什么InputStream,OutputStream;写到try括号里?
放在try()中,InputStream,OutputStream最后会自动关闭,不用手动close;

为什么需要printWriter.flush() ?
发送的数据,先会写入内存的发送缓冲区,等到缓冲区满了,才会写入网卡;这里使用flush(), 使数据立即写入网卡,不用等待缓冲区满;- 客户端与服务器相同,都需要手动flush()一下;

3.3.2 模拟实现客户端

首先创建TcpEchoClient类,类中创建Socekt操作网卡,创建两个参数的构造方法,让客户端能够找到服务器,与服务器来连接;

public class TcpEchoClient {
    private Socket socket = null;
    public TcpEchoClient(String rerverIP, int port) throws IOException {
        socket = new Socket(rerverIP,port);
    }
}

核心逻辑代码:
与UDP类似;

public void start() {
        Scanner scanner = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            //包装为字符流
            PrintWriter printWriter = new PrintWriter(outputStream);
            Scanner scFromSocket = new Scanner(inputStream);//读取网卡请求
            while (true) {
                //1.等待控制台输入
                System.out.println(" --> ");
                String request = scanner.next();//这里next以分隔符\n来分隔每段请求
                //2.发送请求给服务器
                printWriter.println(request);
                printWriter.flush();
                //3.接收服务器响应
                String response = scFromSocket.next();//读取字符流读取字符
                //4.打印日志
                System.out.printf("[%s : %d] request: %s, response: %s\n",socket.getInetAddress().toString(),
                        socket.getPort(),request,response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

TCP客户端-服务器执行过程:
① 服务器先启动,阻塞在accept(),等待客户端连接
② 客户端启动,调用构造方法与服务器建立连接,此时服务器中accept()返回Socket对象;
③ 服务器执行processConnection方法,阻塞到读取客户端请求;
而客户端在调用构造方法后一直往下执行,阻塞等待用户输入,用户输入后读取数据;
④ 客户端读取用户数据发送给服务器,执行到要读取客户端响应后阻塞等待服务器返回响应;
⑤ 服务器接收请求后,读取请求,计算响应,并返回响应;
⑥ 服务器发送响应后重写循环等待下一轮客户端连接;
客户端收到响应,打印结果,再次循环,等待控制台输入;

TCP 客户端总代码:

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("服务器启动!");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            //监听,看绑定的端口是否有客户端连接,如果连接accept就会马上返回Socket对象
            Socket clientSocket = serverSocket.accept();
            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(InputStream inputStrem = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            //为了后续操作
            Scanner scanner = new Scanner(inputStrem);
            PrintWriter printWriter = new PrintWriter(outputStream);
            //执行具体请求
            while (true) {
                //1.读取客户端请求
                //判断客户端是否关闭了
                if(!scanner.hasNext()) {
                    System.out.printf("客户端下线![%s : %d]\n",clientSocket.getInetAddress().toString(),
                            clientSocket.getPort());
                    break;
                }
                String request = scanner.next();
                //2.根据请求做出响应
                String response = process(request);
                //3.返回响应
                printWriter.println(response);
                printWriter.flush();
                //4.打印日志
                System.out.printf("[%s : %d] request: %s, response: %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(8080);
        tcpEchoServer.start();
    }
}

总结

✨✨✨各位读友,本篇分享到内容如果对你有帮助给个赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!

你可能感兴趣的:(计算机网络,计算机网络,java,后端,软件工程)