Java网络编程之UDP与TCP的网络编程

举报
未见花闻 发表于 2022/07/31 21:42:54 2022/07/31
【摘要】 本文将介绍java中的网络编程,常见的网络编程方式有两种,一种是UDP,另外一种是TCP,其中UDP的服务器与客户端之间不需要建立连接就能进行通信,而TCP需要先建立服务器与客户端之间的连接才能进行通信,此外TCP与UDP不能进行通信。

⭐️前面的话⭐️

本文将介绍java中的网络编程,常见的网络编程方式有两种,一种是UDP,另外一种是TCP,其中UDP的服务器与客户端之间不需要建立连接就能进行通信,而TCP需要先建立服务器与客户端之间的连接才能进行通信,此外TCP与UDP不能进行通信。

📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创!
📆华为云首发时间:🌴2022年7月31日🌴
✉️坚持和努力一定能换来诗与远方!
💭参考书籍:📚《java核心技术》,📚《计算机网络》
💬参考在线编程网站:🌐牛客网🌐力扣
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!

🍋1.UDP网络编程

🍒1.1网络编程套接字

我们知道我们的数据是从应用层开始封装,一直到物理层封装完成并发送,那数据传输的第一步就是将应用层的数据交给传输层,为了完成这个过程,操作系统提供了一组API即socket,用来实现将应用层的数据转交给传输层(内核)进一步传输。

常见传输层协议有两种,分别是UDP与TCP,其中UDP无连接,不可靠传输,面向数据报,全双工;TCP有连接,可靠传输,面向字节流,全双工。

其中UDP类型的socket,有两个相关网络传输的核心类,一个是DatagramSocket,其实例的对象表示UDP版本的socket,这个socket可以理解为操作网卡的遥控器。

该类的关键方法是:

  1. receive方法:接收数据。
  2. send方法:发送数据。
  3. close方法:释放资源。

另一个是DatagramPacket,表示UDP数据报,在UDP的服务器和客户端都需要使用到,每次接收和发送数据都是在传输DatagramPacket对象。

最简单的客户端服务器程序就是回显服务,就是服务器收到什么就给客户端发送什么,就像你在一个空旷的地方,大声喊一句,大自然会回你一句一模一样的话。

在网络编程时,一定要注意区分服务器与客户端之间的五元组,所谓五元组就是:

  1. 源IP,本机IP。
  2. 源端口,本机端口号,服务器手动指定,客户端系统随机分配。
  3. 目的IP,包含在数据报中,服务器的目的IP在客户端发来的数据报中,客户端的目的IP就是服务器的IP。
  4. 目的端口,包含在数据报中,服务器的目的端口号在客户端发来的数据报中,客户端的目的端口号就是服务器的端口号。
  5. 协议类型,如UDP,TCP。

🍒1.2UDP客户端服务器回显服务程序

🍇1.2.1UPD服务器

服务器端设计步骤:

  1. 创建Socket实例对象(DatagramSocket对象),并指定服务器的端口号。
  2. 启动服务器。
  3. 获取客户端请求(DatagramPacket为载体)。
  4. 处理客户端请求,并获取计算后的响应。
  5. 发送处理响应(DatagramPacket为载体)。
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    //1. 准备好socket实例,准备传输
    private DatagramSocket serverSocket;
    //2. 创建udp服务器的时候,需要指定端口号进行创建,毕竟你需要知道这个端口号,在你写客户端时,它能帮助找到你的udp服务器
    public UdpEchoServer(int port) throws SocketException {
        this.serverSocket = new DatagramSocket(port);
    }

    //3. 启动服务器
    public void start() throws IOException {
        System.out.println("服务器准备就绪!");
        //UDP不需要建立连接
        while (true) {
            //1. 读取客户端的请求
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);//参数为储存数据的数组与最大空间大小
            serverSocket.receive(requestPacket);
            //2. 解析收到的数据包,一般解析成字符串进行处理
            //构造字符串的参数分别为数据数组,存入数据数组的起始下标,长度,格式
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), "UTF-8");
            //3. 处理请求
            String response = process(request);
            //4. 发送请求,因为数据的传输是依据DatagramPacket来进行传输的,所以我们需要先包装在发送
            //除此之外,我们还需要知道客户端的地址和端口号
            //接收DatagramPacket对象时。该对象里面存有客户端的地址和端口号。可以使用getSocketAddress方法获取
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
                    response.getBytes().length,
                    requestPacket.getSocketAddress());
            serverSocket.send(responsePacket);
            //5. 输出发送日志
            System.out.printf("[%s:%d] 收到的请求: %s, 回应: %s\n",
                    requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
        }
    }
    //4.处理数据,回显服务直接将原数据返回即可
    public String process(String data) {
        return data;
    }

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

因为服务器是被动接收和处理请求的一端,客户端是主动发起请求的一端,那么客户端必须得知道给哪一个服务器发送请求,也就是需要知道服务器的端口号,所以构造客户端的Socket实例对象时,需要指定端口号构造。
我这里给服务器设置的端口号是9090,后面TCP服务器设置的是9092,线程池版本是9091,UDP客户端发送请求的时候注意带上服务器的IP和端口号,后面TCP创建客户端的Socket对象时需要根据服务器的端口号对应创建。

对于请求的处理的部分,因为我们实现的是一个回显服务,所以直接根据传来的请求,返回相同的响应即可,但是在实际开发中,这个处理请求的部分是非常复杂的。

在服务器收到客户端请求时,有关客户端地址的信息也会放入这个请求之中,所以服务器回复响应的时候,可以通过getSocketAddress方法来获取DatagramPacket中服务器的地址。

下面我们来看看客户端部分。

🍇1.2.2UDP客户端

客户端设计步骤:

  1. 创建Socket实例对象(DatagramSocket对象)。
  2. 用户输入请求。
  3. 读取用户请求。
  4. 打包请求并发送给服务器(DatagramPacket对象打包数据)。
  5. 等待响应。
  6. 接收响应,并反馈。
import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    // 1.创建socket对象
    private DatagramSocket clientSocket;
    private String serverIp;//服务器IP地址
    private int serverPort;//服务器端口号
    public UdpEchoClient(String ip, int port) throws SocketException {
        //客户端可以自己指定端口号,也可以让系统自动分配,但是自己指定的端口号可能已经被使用了,所以系统分配端口号更好
        this.clientSocket = new DatagramSocket();
        this.serverIp = ip;
        this.serverPort = port;
    }
    // 2.启动客户端
    public void start() throws IOException {
        //1. 获取用户输入的数据
        Scanner sc = new Scanner(System.in);
        while (true) {
            //请输入
            System.out.print("请输入需要发送的数据->");
            String request = sc.next();
            //2. 根据用户输入的数据,将数据打包,待发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            // 3.发送数据
            clientSocket.send(requestPacket);
            // 4.接收请求
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
            clientSocket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), "UTF-8");
            System.out.printf("我的请求: %s, 它的回应: %s\n", request, response);
        }
    }
    public static void main(String[] args) throws IOException {
        //127.0.0.1表示环回IP,表示自己主机
        UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}

当客户端创建Socket对象时,可以指定端口号创建也可以让系统随机分配,自己指定端口号有个缺点,那就是容易与其他已有的端口号冲突,所以系统随机分配更好,因为不用担心端口号的冲突的问题。

如果需要启动多个客户端,对于idea,需要进行设置,因为idea默认是只支持运行一个客户端的,具体设置过程如下:
第一步,右键代码编辑处,按下图进行操作。
第一步
第二步,找到蓝色小字Modify options,并点击。
第二步
第三步,勾选上Allow multiple instances
第三步
大功告成!

🍒1.3UDP客户端服务器简单翻译服务程序

这个很简单,对于客户端不受影响,服务器只需把处理请求部分的代码修改即可。我们可以使用一个哈希表,将每个单词所对应的汉语意思存入哈希表中,这样就构成了一个词库,然后根据请求来获取对应的汉语词组即可,但是我们不可能把词库建的很全,我们来试着建立几个词汇的词库来帮助我们来理解网络编程即可。

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;

public class UdpDictServer extends UdpEchoServer{
    //最简单的翻译处理服务器
    private final HashMap<String, String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);
        //词库
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("bird", "小鸟");
        dict.put("apple", "苹果");
        dict.put("banana", "香蕉");
        dict.put("strawberry", "草莓");
        dict.put("watermelon", "西瓜");
    }

    @Override
    public String process(String data) {
        return dict.getOrDefault(data, "词库没有该单词!");
    }

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

🍋2. TCP网络编程

🍒2.1TCP客户端服务器回显服务程序

TCP相比于UDP有很大的不同,TCP需要建立连接,并且是通过文件读与写的方式来进行以字节为单位的传输。

而对于TCP传输,Java提供了两个类来进行数据的传输,一个是ServerSocket,给服务器接收客户端的连接,另外一个是Socket,用于服务器与客户端之间的通信,TCP的传输就像是打电话,客户端发送请求后,服务器调用ServerSocket类的accept方法来“接通电话”, 接通后两者之间就可以通过读写文件的方法来进行数据的传输,Socket就相当于一个“媒介”,往里面写数据就是发送,读数据就是接收。

🍇2.1.1TCP服务器

TCP服务器设计步骤:

  1. 创建ServerSocket实例对象,需指定端口号。
  2. 启动服务器。
  3. 使用accept方法建立连接。
  4. 接收请求(通过InputStream类读取请求)。
  5. 处理请求。
  6. 发送响应(通过OutputStream类发送响应)。
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 {
    //1. 创建socket对象
    private ServerSocket serverSocket;

    public TcpEchoServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }
    //2. 启动服务器
    public void start() throws IOException {
        System.out.println("服务器准备就绪!");
        while (true) {
            // 3. 接收客户端的“电话"
            Socket clientSocket = serverSocket.accept();

            // 4. 接收 处理 回应数据
			processContain(clientSocket);
		}
    }
    private void processContain(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 服务器正式与客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
            try (InputStream inputStream = clientSocket.getInputStream()) {
                try (OutputStream outputStream = clientSocket.getOutputStream()) {
                    //接收数据 使用Scanner比InputStream的原生方法read更方便
                    Scanner receiveScanner = new Scanner(inputStream);
                    while (true) {
                        if (!receiveScanner.hasNext()) {
                            System.out.printf("[%s:%d] 服务器与客户端已经断开连接!\n", clientSocket.getInetAddress().toString(),
                                    clientSocket.getPort());
                            break;
                        }
                        String request = receiveScanner.next();
                        //处理数据
                        String response = process(request);
                        //发送数据,为了方便,我们可以使用PrintWriter类将OutputStream类对象包裹起来,就是用来把数据打印到文件里面
                        PrintWriter printWriter = new PrintWriter(outputStream);
                        printWriter.println(response);
                        //及时刷新缓冲区
                        printWriter.flush();
                        //输出回应信息
                        System.out.printf("[%s:%d] 收到的请求: %s  回应: %s\n", clientSocket.getInetAddress().toString(),
                                clientSocket.getPort(), request, response);
                    }
                }
            }catch (IOException e) {
                e.printStackTrace();
            } finally {
                //释放资源 相当于挂断电话
                clientSocket.close();
            }
    }
    public String process(String data) {
        return data;
    }

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

🍇2.1.2TCP客户端

客户端设计步骤:

  1. 创建Socket实例对象,用于与服务器建立连接,参数为服务器的IP地址和端口号。
  2. 启动客户端。
  3. 获取用户的请求。
  4. 发送请求(OutputStream或者PrintWriter)。
  5. 刷新缓冲区。
  6. 等待响应。
  7. 获取响应,并反馈。
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 {
    //创建Socket对象
    private Socket socket;
    public TcpEchoClient(String serverIP, int serverPort) throws IOException {
        socket = new Socket(serverIP, serverPort);
    }
    //启动客户端
    public void start() {
        System.out.println("客户端启动成功!");
        //用户输入数据
        Scanner input = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream()) {
            try (OutputStream outputStream = socket.getOutputStream()) {
                while (true) {
                    //请输入数据
                    System.out.print("请输入需要传输的数据!->");
                    String request = input.next();
                    //发送数据
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    //刷新缓冲区
                    printWriter.flush();
                    //接收回应
                    Scanner receiverScanner = new Scanner(inputStream);
                    String response = receiverScanner.next();
                    //输出数据
                    System.out.printf("我的请求:%s 它的回应:%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", 9092);
        client.start();
    }
}

🍇2.1.3解决无法同时启动多个客户端的问题

但是像上面怎么写,有一个很大的问题,那就是服务器只能连接一个客户端,因为在服务器代码中的processContain方法中,里面还有一层循环,这层循环需要与当前通信的客户端传输完成后才会退出,此时如果有其他的客户端“打电话”过来,就无法跳出到外层循环并与新的客户端建立连接,导致连接上一个客户端后无法与其他客户端建立通话,相当于打电话占线一样。简单说就是一个线程只能连接一个客户端,所以最简单的方式就是使用多线程,我们可以将processContain方法放入到线程执行的任务之中,每连接一个客户端就新建一个线程(使用线程池也可以)去与客户端建立起通信。
多线程

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 {
    //1. 创建socket对象
    private ServerSocket serverSocket;

    public TcpEchoServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }
    //2. 启动服务器
    public void start() throws IOException {
        System.out.println("服务器准备就绪!");
        while (true) {
            // 3. 接收客户端的“电话"
            Socket clientSocket = serverSocket.accept();

            // 4. 接收 处理 回应数据
            Thread thread = new Thread(() -> {
                try {
                    processContain(clientSocket);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            });
            thread.start();
        }
    }
    private void processContain(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 服务器正式与客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
            try (InputStream inputStream = clientSocket.getInputStream()) {
                try (OutputStream outputStream = clientSocket.getOutputStream()) {
                    //接收数据 使用Scanner比InputStream的原生方法read更方便
                    Scanner receiveScanner = new Scanner(inputStream);
                    while (true) {
                        if (!receiveScanner.hasNext()) {
                            System.out.printf("[%s:%d] 服务器与客户端已经断开连接!\n", clientSocket.getInetAddress().toString(),
                                    clientSocket.getPort());
                            break;
                        }
                        String request = receiveScanner.next();
                        //处理数据
                        String response = process(request);
                        //发送数据,为了方便,我们可以使用PrintWriter类将OutputStream类对象包裹起来,就是用来把数据打印到文件里面
                        PrintWriter printWriter = new PrintWriter(outputStream);
                        printWriter.println(response);
                        //及时刷新缓冲区
                        printWriter.flush();
                        //输出回应信息
                        System.out.printf("[%s:%d] 收到的请求: %s  回应: %s\n", clientSocket.getInetAddress().toString(),
                                clientSocket.getPort(), request, response);
                    }
                }
            }catch (IOException e) {
                e.printStackTrace();
            } finally {
                //释放资源 相当于挂断电话
                clientSocket.close();
            }
    }
    public String process(String data) {
        return data;
    }

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

线程池版本:

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 TcpThreadPoolEchoServer {
    //1. 创建socket对象
    private ServerSocket serverSocket;
    public TcpThreadPoolEchoServer(int port) throws IOException {
        this.serverSocket = new ServerSocket(port);
    }
    //2. 启动服务器
    public void start() throws IOException {
        System.out.println("服务器准备就绪!");
        //创建线程池
        ExecutorService pool = Executors.newCachedThreadPool();
        while (true) {
            // 3. 接收客户端的“电话"
            Socket clientSocket = serverSocket.accept();

            // 4. 接收 处理 回应数据
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processContain(clientSocket);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
    private void processContain(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d] 服务器正式与客户端建立连接!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream()) {
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                //接收数据 使用Scanner比InputStream的原生方法read更方便
                Scanner receiveScanner = new Scanner(inputStream);
                while (true) {
                    if (!receiveScanner.hasNext()) {
                        System.out.printf("[%s:%d] 服务器与客户端已经断开连接!\n", clientSocket.getInetAddress().toString(),
                                clientSocket.getPort());
                        break;
                    }
                    String request = receiveScanner.next();
                    //处理数据
                    String response = process(request);
                    //发送数据,为了方便,我们可以使用PrintWriter类将OutputStream类对象包裹起来,就是用来把数据打印到文件里面
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    //及时刷新缓冲区
                    printWriter.flush();
                    //输出回应信息
                    System.out.printf("[%s:%d] 收到的请求: %s  回应: %s\n", clientSocket.getInetAddress().toString(),
                            clientSocket.getPort(), request, response);
                }
            }
        }catch (IOException e) {
            e.printStackTrace();
        } finally {
            //释放资源 相当于挂断电话
            clientSocket.close();
        }
    }
    public String process(String data) {
        return data;
    }

    public static void main(String[] args) throws IOException {
        TcpThreadPoolEchoServer server = new TcpThreadPoolEchoServer(9091);
        server.start();
    }
}

🍒2.2TCP客户端服务器简单翻译服务程序

这个与UDP一样,改一改处理请求过程就行,即加上一个词库就行。

import java.io.IOException;
import java.util.HashMap;

public class TcpDictServer extends TcpEchoServer{
    private final HashMap<String, String> dict = new HashMap<>();
    public TcpDictServer(int port) throws IOException {
        super(port);

        //词库
        dict.put("cat", "小猫");
        dict.put("dog", "小狗");
        dict.put("bird", "小鸟");
        dict.put("apple", "苹果");
        dict.put("banana", "香蕉");
        dict.put("strawberry", "草莓");
        dict.put("watermelon", "西瓜");
    }

    @Override
    public String process(String data) {
        return dict.getOrDefault(data, "词库为找到该单词!");
    }

    public static void main(String[] args) throws IOException {
    	//里面的端口号根据你的服务器对应的端口号进行填写,我这里是9092
        TcpDictServer server = new TcpDictServer(9092);
        server.start();
    }
}

本文完结!撒花!


下期预告:计算机网络之应用层到物理层

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。