首页   >   代码编程

Java Socket编写基于UDP协议的简易聊天室

写完基于TCP协议的聊天室,再来尝试写一个UDP协议的聊天室,由于协议的不同,所以实现起来也是有很大的不同,先来简单的看一下这两个协议的区别:

1、TCP是长连接,UDP是无连接;

2、TCP能保证数据包的正确性,UDP会有丢包;

3、TCP能保证数据包的顺序性,UDP保证不了;

4、TCP对系统资源需求大,UDP不可靠,所以需求小;

5、TCP由于做了一系列的保证,所以速度慢,UDP无需这些保证,所以速度快,实时性高;

6、TCP会有粘包问题,UDP会有分包问题;

关于分包:

UDP受以太网限制,每一个包的最大体积是65507字节,约等于64K。

为什么最大是65507?

因为UDP包头有2个byte用于记录包体长度,2个byte可表示最大值为: 2^16-1 = 64K-1 = 65535,UDP包头占8字节, IP包头占20字节, 65535-28 = 65507。

言归正传:

就拿第一点来说,实现起来跟TCP已经是天壤之别了,如果说不是长连接,那么我们发送消息的形式就需要变通了,TCP中我们是经过服务器转发,也就是说有一个Server的概念,但是在UDP中,这个概念就不存在了,因为UDP擅长的是点对点,我知道了你的Address,那么我就可以直接给你发送数据,也就不需要Server了。

经过上面分析,UDP似乎不太适合做聊天室,如果非要做的话,那就需要硬生生的拉一个节点出来当做Server,先将消息发送到这个节点,然后再由这个节点进行转发(有点儿类似于消息队列的概念)。

仔细想想,如果说单纯的点对点聊天,也不做消息存储之类的功能,那么UDP看起来确实很别扭,还不如直接客户端发送消息,而如果想要做全面的功能:群聊、消息提醒等等,那还是需要一个Server出来主持大局的。。。

服务端代码:

package com.wolffy.socket.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by Felix on 2019/3/26.
 */
public class Server {

    /**
     * socket套接字
     */
    private DatagramSocket socket;

    /**
     * 服务开启的端口号
     */
    private static final int port = 5555;

    /**
     * 缓冲区大小(UDP受以太网限制,每一个包的最大体积是65507字节,约等于64K),本案例中不会发送很长的内容,所以直接1024就够用了
     * <p>
     * 为什么最大是65507?
     * 因为udp包头有2个byte用于记录包体长度. 2个byte可表示最大值为: 2^16-1 = 64K-1 = 65535
     * udp包头占8字节, ip包头占20字节, 65535-28 = 65507
     */
    private static final int buf_size = 1024;

    /**
     * 客户端集合
     */
    private static List<String> clients = new ArrayList<>();

    private void start() {
        try {
            // 创建套接字,打开端口
            socket = new DatagramSocket(port);

            System.out.println("服务开启[" + socket.getLocalSocketAddress() + "],等待客户端连接中...");

            // 启动消息监听线程,这个跟TCP有点儿不一样了,这里无需针对每一个client做消息处理,而是点对点的统一处理
            new MessageListener().start();
        } catch (IOException e) {
            // log
        }
    }

    class MessageListener extends Thread {
        @Override
        public void run() {
            try {
                DatagramPacket packet;
                String msg, client;
                // 循环监听消息,后面可以考虑加上flag规避死循环
                while (true) {
                    // 组包
                    packet = new DatagramPacket(new byte[buf_size], buf_size);
                    // 接收消息
                    msg = receiveMsg(packet);
                    // 解析出client
                    client = packet.getSocketAddress().toString();
                    // 不能再用TCP那一套了,只能通过消息来确定上线与否
                    // 因为UDP发了就不管了,所以默认只要发了消息就一定在线,下线只能通过心跳和主动退出
                    if ("online".equals(msg)) {
                        clients.add(client);
                        System.out.println("客户端[" + client + "]连接成功,当前在线客户端" + clients.size() + "个");
                        sendMsg(0, "[系统消息]:欢迎" + client + "来到聊天室,当前共有" + clients.size() + "人在聊天", client);
                    } else {
                        sendMsg(1, "[" + client + "]:" + msg, client);
                    }
                }
            } catch (IOException e) {
                // log
            }
        }
    }

    /**
     * 解析出client的IP
     *
     * @param address socket地址,如:/127.0.0.1:9524
     * @return IP
     */
    private String getIp(String address) {
        String[] array = address.split(":");
        String s = array[0];
        return s.substring(1);
    }

    /**
     * 解析出client的端口
     *
     * @param address socket地址,如:/127.0.0.1:9524
     * @return 端口
     */
    private int getPort(String address) {
        String[] array = address.split(":");
        String s = array[1];
        return Integer.valueOf(s);
    }

    /**
     * 发送消息
     *
     * @param type   消息类型(0、系统消息;1、用户消息)
     * @param msg    消息内容
     * @param client 客户端(用来作比对,是否跳过自己)
     * @throws IOException
     */
    private void sendMsg(int type, String msg, String client) throws IOException {
        if (type != 0) {
            System.out.println("处理消息:" + msg);
        }
        DatagramPacket send_packet;
        byte[] bytes;
        for (String address : clients) {
            if (type != 0 && client.equals(address)) {
                continue;
            }
            // 将需要发送的消息,转换成byte数组
            bytes = msg.getBytes();
            // 组装发送包,需要指定:包内容、包长度、目标IP、目标端口
            send_packet = new DatagramPacket(bytes, bytes.length, InetAddress.getByName(getIp(address)), getPort(address));
            // 发送
            socket.send(send_packet);
        }
    }

    /**
     * 接收消息,由于receive是阻塞方法,所以可以直接将packet包当参数传进来
     *
     * @param packet 接收的数据包
     * @return 消息内容
     * @throws IOException
     */
    private String receiveMsg(DatagramPacket packet) throws IOException {
        // 组装接收包,需要指定:包内容、包长度,这里的包内容只是先声明一个空数组,等待数据的填充
        // DatagramPacket packet = new DatagramPacket(new byte[buf_size], buf_size);
        socket.receive(packet);
        // 包内容都是字节数组,需要转换成字符串
        return new String(packet.getData(), packet.getOffset(), packet.getLength());
    }

    public static void main(String[] args) {
        new Server().start();
    }

}

客户端代码:

package com.wolffy.socket.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Random;
import java.util.Scanner;

/**
 * Created by Felix on 2019/3/26.
 */
public class Client {

    /**
     * socket套接字
     */
    private DatagramSocket socket;

    /**
     * 缓冲区大小(UDP受以太网限制,每一个包的最大体积是65507字节,约等于64K),本案例中不会发送很长的内容,所以直接1024就够用了
     * <p>
     * 为什么最大是65507?
     * 因为udp包头有2个byte用于记录包体长度. 2个byte可表示最大值为: 2^16-1 = 64K-1 = 65535
     * udp包头占8字节, ip包头占20字节, 65535-28 = 65507
     */
    private static final int buf_size = 1024;

    /**
     * 服务器的IP
     */
    private static final String server_ip = "127.0.0.1";

    /**
     * 服务器的端口
     */
    private static final int server_port = 5555;

    private void start() {
        try {
            // 创建套接字,打开端口
            socket = new DatagramSocket(getPort());

            System.out.println("连接服务器成功,身份证:[" + socket.getLocalSocketAddress() + "]");

            // 由于不是TCP,所以需要手动发送上线的消息通知服务端
            sendMsg("online");

            // 开启发送消息的线程
            new SendMessageListener().start();
            // 开启接收消息的线程
            new ReceiveMessageListener().start();
        } catch (IOException e) {
            // log
        }
    }

    /**
     * UDP不会自动生成客户端的端口(确切来说,UDP是么有服务端和客户端的区分的),需要自己生成
     *
     * @return 4位数的端口
     */
    private int getPort() {
        Random random = new Random();
        String s = "";
        for (int i = 0; i < 4; i++) {
            s += random.nextInt(9) + 1;
        }
        return Integer.valueOf(s);
    }

    /**
     * 发送消息
     *
     * @param msg 消息内容
     */
    private void sendMsg(String msg) {
        try {
            // 将需要发送的消息,转换成byte数组
            byte[] bytes = msg.getBytes();
            // 组装发送包,需要指定:包内容、包长度、目标IP、目标端口
            DatagramPacket send_packet = new DatagramPacket(bytes, bytes.length, InetAddress.getByName(server_ip), server_port);
            // 发送
            socket.send(send_packet);
        } catch (IOException e) {
            // log
        }
    }

    /**
     * 接收消息
     *
     * @return 消息内容
     */
    private String receiveMsg() {
        try {
            // 组装接收包,需要指定:包内容、包长度,这里的包内容只是先声明一个空数组,等待数据的填充
            DatagramPacket receive_packet = new DatagramPacket(new byte[buf_size], buf_size);
            socket.receive(receive_packet);
            // 包内容都是字节数组,需要转换成字符串
            return new String(receive_packet.getData(), receive_packet.getOffset(), receive_packet.getLength());
        } catch (IOException e) {
            // log
        }
        return null;
    }

    class SendMessageListener extends Thread {
        @Override
        public void run() {
            // 监听console输入
            Scanner scanner = new Scanner(System.in);
            while (true) {
                sendMsg(scanner.next());
            }
        }
    }

    class ReceiveMessageListener extends Thread {
        @Override
        public void run() {
            // 监听消息的接收,死循环
            while (true) {
                System.out.println(receiveMsg());
            }
        }
    }

    public static void main(String[] args) {
        new Client().start();
    }

}

服务端效果图:

Java Socket编写基于UDP协议的简易聊天室

客户端效果图:

Java Socket编写基于UDP协议的简易聊天室

效果跟之前TCP实现的聊天室一模一样,至于心跳、主动退出这些功能,就不再重复编写了,实现思路都是一样的!

详细思路,见文章:Java Socket聊天室实现心跳

QQ群: 686430774  /  718410762

站长Q: 1347384268

如果文章有帮到你,可以考虑请博主喝杯咖啡!

分享到:

欢迎分享本文,转载请注明出处!

作者:不忘初心

发布时间:2019-03-29

永久地址:https://www.jiweichengzhu.com/article/4d4bd20aaa524b0d961fb0f1b744b3a4

评论