Java网络编程

UDP和TCP网络协议,基于Socket的UDP和TCP网络编程的介绍。

Author: Msuenb

Date: 2023-02-21


网络基础知识

每个计算设备上都有若干个网卡,每个网卡上有(全球唯一)单独的硬件地址:MAC地址,全称是Media Access Control Address,介质访问控制地址。

IP地址

每个网卡/机器都有一个或多个IP地址,在计算机网络中,依靠IP地址来定义一台计算机的位置。IP地址有两个版本:

  • IPV4:如,192.168.0.100,每段从0 - 255。
  • IPV6:128bit长,分成8端,每段4个16进制数,全是0的字段可以省略,如,FE::::AAAA::00C2:0002

查询当前机器联网后的IP地址:可以用ipconfig(Windows)、ifconfig(Mac/Linux)查询

保留IP:127.0.0.1 指本机

port端口

  每台计算机有 0~65535 个端口,其中 0~1023 这1024个端口,OS已经占用了,操作系统会利用这些端口做一些特殊的事情。如,80是Web服务,23是telnet。剩下的端口 1024~65535 是一般程序可以使用(谨防冲突)。

  两台机器通讯就是建立在IP+Port上进行的,数据从一个IP的port出发(发送方),运输到另一个IP的port(接收方)。

  在Windows、Linux、Mac上都可以通过netstat -an来查询当前机器和和其他机器同讯时的 IP 和 port。

通信协议

  • UDP: User Datagram Protocol

    用户数据报协议,面向无连接的协议,它不保证可靠的数据传输。但是它的速度比较快,而且对可以在较差的网络下使用。

  • TCP: Transmission Control Protocol

    传输控制协议,面向连接的协议,它可以做到两台机器的可靠的、无差错的数据传输。但是它对网络的要求比较高,网络必须持续稳定,而且还要保证速度还可以。

UDP网络编程

UDP是一个无连接、无状态的通讯协议,具体表现是:发送方发送消息,如果接受方刚好在目的地,则可以接受;如果接收方不在目的在,则这个消息就会丢失。发送方也无法得知消息是否发送成功。UDP的好处就是简单、节省、经济。

在Java里面主要靠以下这两个类来实现UDP通信:

  • DatagramSocket:通讯数据管道,需要绑定在某个IP和Port
  • DatagramPacket:集装箱,用来封装数据,上面会有一个地址标签:目的地IP+Port

发送端示例代码:

import java.net.*;

public class UDPSend {
    public static void main(String[] args) throws Exception {
        // 1. 建立发送端的DatagramSocket
        DatagramSocket ds = new DatagramSocket();

        // 接受方的ip 和端口号
        InetAddress ip = InetAddress.getByName("127.0.0.1");
        int port = 9001;

        byte[] bytes = "hello, world!".getBytes();
        // 2. 建立数据包
        DatagramPacket dp = new DatagramPacket(bytes, 0, bytes.length, ip, port);

        // 3. 发送数据
        ds.send(dp);

        // 4. 关闭资源
        ds.close();
    }
}

接收端示例代码:

import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPReceive {
    public static void main(String[] args) throws Exception{
        // 1. 建立接受端DatagramSocket 并指定端口号
        DatagramSocket ds = new DatagramSocket(9001);

        byte[] buf = new byte[1024];
        // 2. 创建数据包
        DatagramPacket dp = new DatagramPacket(buf, buf.length);

        // 3. 接受数据 若没有收到消息 一直阻塞
        ds.receive(dp);

        System.out.println(new String(dp.getData(), 0, dp.getLength()));

        // 4. 关闭资源
        ds.close();
    }
}

TCP网络编程

TCP: Transmission Control Protocol,即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议,它能够做到两台机器之间进行可靠的、无差错的数据传输。

TCP协议会采用三次握手方式让它们建立一个连接,用于发送和接收数据的虚拟链路。数据传输完毕TCP协议会采用四次挥手方式断开连接。

单客户端与服务器单次通信

需求:客户端连接服务器,连接成功后给服务发送“hello”,服务器收到消息后,给客户端返回“欢迎登录”,客户端接收消息后,断开连接。

TCP服务器示例代码

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class TCPServer {
    public static void main(String[] args) throws Exception{
        // 1. 创建一个ServerSocket对象 绑定9999端口
        ServerSocket server = new ServerSocket(9999);
        System.out.println("等待连接...");

        // 2. 监听客户端的连接请求  没有则阻塞
        Socket socket = server.accept();
        InetAddress inetAddress = socket.getInetAddress();
        System.out.println(inetAddress.getHostAddress() + " 客户端连接成功");

        // 3. 获取输入流 用来接受客户端发来的消息
        InputStream is = socket.getInputStream();
        // 接受数据
        StringBuilder sb = new StringBuilder();
        byte[] buf = new byte[1024];
        int len;
        while ((len = is.read(buf)) != -1) {
            sb.append(new String(buf, 0, len));
        }
        System.out.println(inetAddress.getHostAddress() + ": " + sb);

        // 4. 获取输出流 用来发送数据给客户端
        OutputStream os = socket.getOutputStream();
        // 发送数据
        os.write("欢迎登录".getBytes());
        os.flush();

        // 5. 关闭socket
        socket.close();
    }
}

TCP客户端示例代码

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class TCPClient {
    public static void main(String[] args) throws Exception{
        // 1.准备Socket 连接服务器 需要指定服务器的IP和Port
        Socket socket = new Socket("127.0.0.1", 9999);

        // 2. 获取输出流 发送数据给服务器
        OutputStream os = socket.getOutputStream();
        // 发送数据
        os.write("hello".getBytes());
        os.flush();
        // socket.shutdownOutput();    // 写入 -1 标记流的末尾

        // 3. 获取输入流 用来接受服务器发来的信息
        InputStream is = socket.getInputStream();
        StringBuilder sb = new StringBuilder();
        byte[] buf = new byte[1024];
        int len;
        while ((len = is.read(buf)) != -1) {
            sb.append(new String(buf, 0, len));
        }
        System.out.println("服务器:" + sb);

        // 4.关闭socket
        socket.close();
    }
}

多客户端与服务器多次通信

通常情况下,服务器不应该只接受一个客户端请求,而是不断地接受来自客户端的所有请求,不断地调用ServerSocket的accept()方法。

如果服务器端要“同时”处理多个客户端的请求,因此服务器端需要为每一个客户端单独分配一个线程来处理,否则无法实现“同时”。

案例需求:多个客户端连接服务器,并进行多次通信

  • 每一个客户端连接成功后,从键盘输入英文单词或中国成语,并发送给服务器
  • 服务器收到客户端的消息后,把词语“反转”后返回给客户端
  • 客户端接收服务器返回的“词语”,打印显示
  • 当客户端输入“stop”时断开与服务器的连接
  • 多个客户端可以同时给服务器发送“词语”,服务器可以“同时”处理多个客户端的请求

服务端示例代码:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {
    public static void main(String[] args) throws IOException {
        // 1.准备一个ServerSocket
        ServerSocket server = new ServerSocket(9999);
        System.out.println("等待连接...");

        int count = 0;
        while (true) {
            // 2. 监听客户端的连接
            Socket socket = server.accept();
            System.out.println("第" + ++count + "个客户端: " +
                    socket.getInetAddress().getHostAddress() + " 连接成功");

            ClientHandlerThread ct = new ClientHandlerThread(socket);
            ct.start();
        }
    }

    static class ClientHandlerThread extends Thread {
        private Socket socket;
        private String ip;

        public ClientHandlerThread(Socket socket) {
            this.socket = socket;
            this.ip = socket.getInetAddress().getHostAddress();
        }

        @Override
        public void run() {
            try {
                // 1. 获取输入流 接受客户端发来的信息
                BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                // 2. 获取输出流  用来发送数据给客户端
                PrintStream out = new PrintStream(socket.getOutputStream());

                String str;
                // 3. 不断接受客户端发送的数据
                while ((str = input.readLine()) != null) {
                    // 反转 并返回给客户端
                    StringBuilder sb = new StringBuilder(str);
                    sb.reverse();
                    out.println(sb);
                }
                System.out.println("客户端" + ip + "正常退出");
            } catch (IOException e) {
                System.out.println("客户端" + ip + "异常退出");
            } finally {
                try {
                    socket.close(); // 4 断开连接
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端示例代码:

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class Client {
    public static void main(String[] args) throws IOException {
        // 1. 准备Socket 连接到服务器
        Socket socket = new Socket("127.0.0.1", 9999);

        // 2. 获取输出流 用来发送数据给客户端
        PrintStream out = new PrintStream(socket.getOutputStream());

        // 3. 获取输入流 用来接受服务端数据
        BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));

        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.println("输入发给服务器的单词或成语:");
            String msg = scanner.nextLine();
            if (msg.equals("quit")) {
                socket.shutdownOutput();
                break;
            }
            // 4. 发送数据
            out.println(msg);
            // 接受数据
            String receive = input.readLine();
            System.out.println("服务器:" + receive);
        }

        // 5 关闭socket 断开与服务器连接
        scanner.close();
        socket.close();
    }
}
posted @ 2023-02-21 21:02  msuenb  阅读(16)  评论(0编辑  收藏  举报