【JavaSE】十七、UDP套接字编程 && TCP套接字编程

文章目录

Ⅰ. UDP 和 TCP 的区别

特性 TCP(传输控制协议) UDP(用户数据报协议)
类型 面向连接、面向字节流 无连接、面向数据报
可靠性 可靠,数据按顺序到达、无丢包 不可靠,可能丢包、乱序
缓冲区 既有接收缓冲区,也有发送缓冲区 只有接收缓冲区
速度 较慢(需握手、确认等) 较快(没有连接管理)
应用场景 文件传输、Web、聊天 视频直播、游戏、广播
Java支持类 SocketServerSocket DatagramSocketDatagramPacket

Ⅱ. UDP 套接字编程

一、常用方法

  1. UDP 是无连接的,因此每次发送数据报前都需要指定目标地址和目标端口(而 TCP 则只需要创建套接字时候绑定即可)
  2. Java创建一个 DatagramSocket 对象,就是在操作系统中打开了一个 socket 文件,通过这个文件,可以读写数据,而该文件负责将内容与网卡进行交互,从而达到网络通信功能。
  3. DatagramPacket 就是一个数据报,DatagramSocket 是真正的套接字文件,使用时候就是通过 DatagramSocket 中的 send/receive 方法来传输一个个数据报 DatagramPacket
类名 返回值 功能
DatagramSocket() 构造方法 创建一个 UDP 套接字,系统会随机分配一个可用端口
DatagramSocket(int port) 构造方法 绑定到指定端口
DatagramSocket(int port, InetAddress laddr) 构造方法 绑定到指定 IP 和端口
DatagramSocket(SocketAddress bindaddr) 构造方法 使用 InetSocketAddress 绑定地址和端口
send(DatagramPacket p) void 发送一个数据报包到目标地址
receive(DatagramPacket p) void 阻塞接收一个数据报包
close() void 关闭套接字,释放资源
isClosed() boolean 判断套接字是否已关闭
setSoTimeout(int timeout) void 设置接收数据的超时时间(毫秒)
getSoTimeout() int 获取当前接收超时时间
setReuseAddress(boolean on) void 设置是否允许地址重用
getLocalPort() int 获取本地绑定的端口号
getLocalAddress() InetAddress 获取本地绑定的 IP 地址
DatagramPacket(byte[] buf, int length) 构造方法 创建空数据报,用于接收数据
DatagramPacket(byte[] buf, int length, InetAddress addr, int port) 构造方法 创建用于发送的数据报,指定目标地址和端口
DatagramPacket(byte[] buf, int offset, int length, InetAddress addr, int port) 构造方法 发送指定字节数据,支持偏移和长度控制
DatagramPacket(byte buf[], int offset, int length, SocketAddress address) 构造方法 创建数据报,通过 SocketAddress 直接指定端口和地址
getData() byte[] 获取数据报的缓冲区内容
setData(byte[] buf) void 设置数据缓冲区
getLength() int 获取数据报中有效数据长度
setLength(int length) void 设置数据报的有效数据长度
getSocketAddress() SocketAddress 获取地址、端口的一个结构体 相当于下面两个方法的结合
getAddress() InetAddress 获取目标地址或发送者地址
getPort() int 获取目标端口或发送者端口
setAddress(InetAddress addr) void 设置数据包的目标地址
setPort(int port) void 设置数据包的目标端口

💥注意事项:

  1. 要构造 InetAddress 的话,需要调用 InetAddress.getByName(s) 静态方法 ,其中 s 是地址字符串,比如 "127.0.0.1",最后就能拿到的就是地址为 sInetAddress 对象!
  2. 一般构造 DatagramPacketgetSocketAddress() 的版本比较方便,因为这个方法实际上就包括了端口和地址,如下图所示:
  1. 创建 DatagramPacket 的时候要传入的 byte 数组,实际上是一个应用层的缓冲区 ,通常建议缓冲区大小设置为 大于或等于预期最大 UDP 包大小 ,比如 40968192
    1. 此外,要使用 req.getLength() 来获取实际接收数据的长度,而不是 buf.length ,即在读取数据时要用 new String(req.getData(), 0, req.getLength()),避免读到多余的空字节。

二、服务端

  1. 接收客户端发来的请求,并且进行解析
  2. 处理请求
  3. 封装成数据报进行响应
java 复制代码
/**
 * 服务器不需要知道数据从哪里来,所以不需要用字段来存放客户端的IP和端口,直接从请求中获取即可
 */
public class Server {
    private DatagramSocket socket = null; // 通信就靠DatagramSocket对象

    public Server(int port) throws SocketException {
        socket = new DatagramSocket(port); // 服务端需要指定port,让别人来连接
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while(true) {
            // 1. (阻塞)接收客户端发来的请求,并且进行解析
            DatagramPacket req = new DatagramPacket(new byte[4096], 4096);
            socket.receive(req);
            String data = new String(req.getData(), 0, req.getLength()); // 从 DatagramPacket 取到有效的数据

            // 2. 处理请求
            String outcome = func(data);

            // 3. 封装成数据报进行响应(注意要填写目的端口和ip)
            DatagramPacket resp = new DatagramPacket(outcome.getBytes(),
                    0,
                    outcome.getBytes().length,
                    //req.getAddress(), req.getPort()); // 可以这样子写,但是麻烦,推荐下面的写法
                    req.getSocketAddress());
            socket.send(resp);

            // 打印日志,看看效果
            System.out.printf("[%s:%d] req: %s, resp: %s\n", req.getAddress(), req.getPort(), data, outcome);
        }
    }
    
    // 业务代码(不是现在的重点)
    public String func(String data) {
        return data;
    }

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

三、客户端

  1. 构造请求数据报,注意要传入目的端口和IP到数据报中
  2. 发送请求到服务器
  3. 接收服务器发来的响应,然后进行解析
java 复制代码
/**
 * 1. 因为客户端在发送数据报的时候需要知道服务器的IP和端口,并且由于UDP
 *    每次发送数据都得传入服务器的IP和端口,所以需要用单独的字段来保存
 *
 * 2. 创建DatagramSocket时候传入的端口是指定客户端自己在本机的端口,
 *    而不是服务器的端口,注意和上面区分开!
 */
public class Client {
    private int port;    // 服务器的端口
    private String addr; // 服务器的地址
    private DatagramSocket socket = null;

    public Client(int port, String addr) throws SocketException {
        this.port = port;
        this.addr = addr;
        socket = new DatagramSocket(); // 让系统自动分配端口号
    }

    public void start() throws IOException {
        System.out.println("客户端启动!");
        Scanner sc = new Scanner(System.in);

        while(true) {
            // 1. 构造请求数据报,注意要传入目的端口和IP到数据报中
            System.out.print("请输入要发送给服务器的信息:");
            String message = sc.nextLine();
            DatagramPacket req = new DatagramPacket(message.getBytes(),
                    0,
                    message.getBytes().length,
                    InetAddress.getByName(addr), // 利用静态方法getByName构造InetAddress
                    port);

            // 2. 发送请求到服务器
            socket.send(req);

            // 3. 接收服务器发来的响应,然后进行解析
            DatagramPacket resp = new DatagramPacket(new byte[4096], 4096);
            socket.receive(resp);
            String data = new String(resp.getData(), 0, resp.getLength());

            // 进行日志输出,查看效果
            System.out.println("响应:" + data);
        }
    }

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

运行效果如下所示:

Ⅲ. TCP 套接字编程

一、常用方法

  1. TCP 连接只需要在创建套接字时候绑定端口和地址即可 ,而不需要像 UDP 一样每次发送数据的时候都要指定目标端口和地址!
  2. ServerSocket 这个类主要负责建立连接、监听新连接,而不负责数据的接收和发送!
  3. Socket 这个类主要负责数据的接收和发送!
    1. 因为 TCP 是面向字节流的,所以实际上数据的接收和发送,都是通过 Socket 获取套接字文件的输入输出流 InputStream/OutputStream,然后以字节为单位,来处理该底层套接字 ,本质还是文件 IO
  4. Java 中,如果你没有显式调用 connect() 创建连接,但你直接使用 Socket.getOutputStream().write(...) 来写数据,则系统会在你第一次写数据的时候自动调用 connect() 建立连接!
方法 / 构造方法 返回值类型 功能说明
ServerSocket() 构造方法 创建未绑定的服务器套接字
ServerSocket(int port) 构造方法 创建并绑定到指定端口的套接字
ServerSocket(int port, int backlog) 构造方法 指定端口与连接请求队列长度
ServerSocket(int port, int backlog, InetAddress bindAddr) 构造方法 指定端口、队列长度和绑定地址
accept() Socket 阻塞等待客户端连接,返回通信用的 Socket
close() void 关闭服务器套接字,释放资源
isClosed() boolean 判断服务器套接字是否关闭
getInetAddress() InetAddress 获取绑定的本地 IP 地址
getLocalPort() int 获取绑定的本地端口
setSoTimeout(int timeout) void 设置 accept() 阻塞的超时时间(毫秒)
getSoTimeout() int 获取当前 accept() 超时时间
Socket() 构造方法 创建未连接的套接字(用于延迟连接)
Socket(String host, int port) 构造方法 创建并连接到指定主机和端口
Socket(InetAddress address, int port) 构造方法 同上,使用 IP 地址连接
getInputStream() InputStream 获取输入流,用于接收数据
getOutputStream() OutputStream 获取输出流,用于发送数据
close() void 关闭连接,释放资源
isClosed() boolean 判断是否关闭连接
isConnected() boolean 判断是否连接成功
isBound() boolean 判断是否已绑定本地地址
getInetAddress() InetAddress 获取远程地址
getPort() int 获取远程端口号
getLocalAddress() InetAddress 获取本地地址
getLocalPort() int 获取本地端口号
setSoTimeout(int timeout) void 设置输入流读取的超时时间
getSoTimeout() int 获取读取超时时间

二、服务端

  1. 监听新连接
  2. 创建新线程来处理新连接 Socket,防止主线程阻塞
    1. 读取请求并进行解析
    2. 将解析后的请求进行业务处理
    3. 响应结果给客户端
  3. 关闭 Socket,防止资源泄露问题

这里需要关闭 Socket 对象的原因是一个服务器会创建很多新线程来处理不同的连接,这些连接本质都是套接字文件,如果没有释放文件资源的话,最后就会导致资源泄露问题!

至于 ServerSocketDatagramSocket 为什么不需要释放,是因为它们全局只有一个对象,而且生命周期是随着程序生命周期的,所以不会出现资源泄露问题!

此外 Scanner PrintWriter 也不需要释放,因为它们是从 Socket.getXXX() 获得的,本质还是 Socket 对象套接字文件对应的流对象,所以不需要关心 Scanner 和 PrintWriter 的释放问题!

💥注意事项:

  1. 因为 Scanner 这里使用的是 nextLine()hasNextLine(),所以 PrintWriter 在使用的时候不能用 write(),而要用 println(),因为 write() 是不带换行符的,此时就算回车结束了,字符串也不会带回车,那么进入判断语句中就卡在那里了!
    1. 结论:用 Scanner.nextLine() 读取,就一定要 println() 写入
  2. 在用 PrintWriter 写入数据到套接字文件后,要调用 flush() 进行缓冲区刷新,不然只能缓冲区快满了自动刷新!

① 多线程版本

java 复制代码
/**
 *  服务器只需要指定端口即可
 */
public class Server {
    private ServerSocket socket = null;

    public Server(int port) throws IOException {
        socket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("TCP服务器启动!");
        while(true) {
            // 1. 监听新连接
            Socket conn = socket.accept();
            System.out.println("获取到新连接:" + conn.getInetAddress() + "/" + conn.getPort());

            // 2. 创建新线程来处理新连接,防止主线程阻塞
            new Thread(() -> {
                work(conn);
            }).start();
        }
    }

    // 新连接实际上要处理的任务
    private void work(Socket conn) {
        try (InputStream in = conn.getInputStream();
             OutputStream out = conn.getOutputStream();
             Scanner sc = new Scanner(in);
             PrintWriter pw = new PrintWriter(out)) {
            while(true) {
                // 3. 读取请求并进行解析
                if(sc.hasNextLine() == false) {
                    // 要判断是否断开连接:如果客户端断开连接了,则会返回false
                    System.out.printf("[%s:%d] 客户端下线!\n", conn.getInetAddress().toString(), conn.getPort());
                    break;
                }
                String req = sc.nextLine();

                // 4. 将解析后的请求进行业务处理
                String resp = func(req);

                // 5. 响应结果给客户端
                pw.println(resp); // ✔自动添加换行符,满足服务端 Scanner.nextLine(),使用write则会死循环!
                pw.flush();       // ❗❗❗细节❗❗❗

                // 搞一下日志输出看看效果
                System.out.printf("[%s:%d] req: %s, resp: %s\n", conn.getInetAddress().toString(), conn.getPort()
                                                               , req, resp);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // 6. 释放Socket资源
            try {
                conn.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    private String func(String data) {
        return data;
    }

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

② 线程池版本

只需要改动上面 start() 函数里面创建线程的方式即可:

java 复制代码
public void start() throws IOException {
        System.out.println("TCP服务器启动!");
        while(true) {
            // 监听新连接
            Socket conn = socket.accept();
            System.out.println("获取到新连接:" + conn.getInetAddress() + "/" + conn.getPort());
            
            // 创建线程池来处理新连接,推荐用newCachedThreadPool
            ExecutorService pool = Executors.newCachedThreadPool();
            pool.submit(() -> {
                work(conn);
            });
        }
    }

三、客户端

  1. 输入数据,写入 Socket 对象中,然后进行刷新
  2. 接收服务器的响应
java 复制代码
public class Client {
    private Socket conn = null;

    public Client(String addr, int port) throws IOException {
        conn = new Socket(addr, port);
    }

    public void start() {
        System.out.println("客户端启动!");
        Scanner tmp = new Scanner(System.in);
        try (InputStream in = conn.getInputStream();
             OutputStream out = conn.getOutputStream();
             Scanner sc = new Scanner(in);
             PrintWriter pw = new PrintWriter(out)) {
            while(true) {
                // 1. 输入数据,写入Socket中,然后刷新
                System.out.print("请输入要发送的数据:");
                String req = tmp.nextLine();
                pw.println(req);  // ✔自动添加换行符,满足服务端 Scanner.nextLine(),使用write则会死循环!
                pw.flush();       // ❗❗❗细节❗❗❗

                // 2. 接收服务器的响应
                if(sc.hasNextLine() == false) {
                    System.out.println("客户端断开连续!");
                    break;
                }
                String resp = sc.nextLine();

                // 打印日志看看效果
                System.out.printf("[%s:%d] req: %s, resp: %s\n", conn.getInetAddress().toString(),
                                                                 conn.getPort(),
                                                                 req, resp);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws IOException {
        Client client = new Client("127.0.0.1", 9090);
        client.start();
    }
}
相关推荐
揪住海1 天前
UDP网络巩固知识基础题(1)
网络·udp
IT19951 天前
C++使用“长度前缀法”解决TCP“粘包 / 拆包”问题
服务器·网络·c++·tcp/ip
while(1){yan}1 天前
网络协议TCP
java·网络·网络协议·tcp/ip·青少年编程·电脑常识
logic_51 天前
DHCP+DNS
网络安全·udp·信号处理
电子科技圈1 天前
SiFive车规级RISC-V IP获IAR最新版嵌入式开发工具全面支持,加速汽车电子创新
嵌入式硬件·tcp/ip·设计模式·汽车·代码规范·risc-v·代码复审
谈笑也风生1 天前
验证IP地址(三)
python·tcp/ip·mysql
福尔摩斯张1 天前
TCP协议深度解析:从报文格式到连接管理(超详细)
linux·c语言·网络·c++·笔记·网络协议·tcp/ip
Sleepy MargulisItG1 天前
【Linux网络编程】UDP Socket
linux·网络·udp
JXNL@1 天前
网通领域核心设备解析:CPE、IP Phone 与 AP 技术全指南
网络·网络协议·tcp/ip