当前位置:   article > 正文

网络编程—TCP、UDP编程_tcp udp 编程

tcp udp 编程

一、网络编程概述

1.什么是网络编程

​ 网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)

当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。

发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。

接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。

2.Socket套接字

​ Socket套接字,是由系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程

系统提供的socket api有两种:

UDP

  • 无连接
  • 不可靠传输
  • 面向数据报
  • 有接收缓冲区,无发送缓冲区
  • 大小受限:一次最多传输64k
  • 全双工

TCP

  • 有连接
  • 可靠传输
  • 面向字节流
  • 有接收缓冲区,也有发送缓冲区
  • 大小不限
  • 全双工

二、UDP编程

1.UDP套接字

DatagramSocket 是UDP Socket,用于发送和接收UDP数据报

构造方法作用
DatagramSocket()创建一个UDP数据报套接字的Socket, 绑定到任意一个随机端口号(一般用于客户端,自动分配端口)
DatagramSocket(int port)创建一个UDP数据报套接字的Socket, 绑定到本机指定端口(一般用于服务器)
方法作用
void receive(DatagramPacket p)从此套接字接收数据 (如果没有接收到数据报, 进行阻塞等待)
void send(DatagramPacket p)从此套接字发送数据包 (不会阻塞等待,直接发送)
void close()关闭此数据报套接字

DatagramPacket是UDP Socket发送和接收的数据报

构造方法作用
DatagramPacket(byte[] buf, int length)构造一个DatagramPacket用来接收数据报,接收的数据保存在字节数组里, 接受指定长度(一般用于读取信息)
DatagramPacket(byte[] buf, int offset,int length, SocketAddress address)构造一个DatagramPacket用来发送数据报,发送的数据为字节数组,从0到指定长度,address用来指定目的主机的IP和端口号(服务器可以直接获取IP和端口号,客户端需要自行指定)
关键方法作用
InetAddress getAddress()从接受的数据报中,获取发送端IP地址,或从发送的数据报中,获取接收端主机IP地址
int getPort()从接收的数据报中,获取发送端主机的端口号,或从发送的数据报中,获取接收端的端口号
SocketAddress getSocketAddress()从接收的数据报中,获取发送端主机SocketAddress,或从发送的数据报中,获取接收端的SocketAddress(IP地址+端口号
byte[] getData()获取数据报的数据

2.UDP服务器

1.创建一个DatagramSocket对象,指定端口号,客户端通过这个端口号发送消息

2.通过DatagramPacket对象获取到客户端发送的消息,并且使用receive()填充

3.处理获取到的消息,先使用new String()把字节转换成字符

4.服务器接收到消息后,使用DatagramPacket对象封装给客户端反馈信息,反馈的消息必须是字节形式,再使用send()方法发送反馈消息

public class Server {

    //准备好socket实例,准备传输
    private DatagramSocket socket = null;

    //构造时指定服务器的端口号
    public Server(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        //服务器要给多个客户端提供服务
        while (true) {
            //1. 读取客户端发过来的信息
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //receive内部会对参数对象进行填充数据,填充的数据来源于网卡
            socket.receive(requestPacket);
            //解析收到的数据包成String
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //2. 根据请求计算响应
            String response = process(request);
            //处理响应内容: 字符 => 字节
            byte[] bytes = response.getBytes();
            //3. 把响应写回客户端
            //requestPacket.getSocketAddress()通过数据报获取到ip+端口号
            DatagramPacket responsePacket = new DatagramPacket(bytes,bytes.length,requestPacket.getSocketAddress());
            socket.send(responsePacket);
            //输出一下发送日志
            System.out.printf("日志信息:  [%s:%d] 接收到的消息: %s, 反馈消息: %s \n",requestPacket.getAddress().toString(),requestPacket.getPort(),request, response);
        }
    }

    //处理接收到的数据
    public String process(String request) {
        System.out.println("客户端发送的消息:"+request);
        String response = "服务器已接收到信息";
        return response;
    }

    public static void main(String[] args) throws IOException {
        Server server = new Server(9090);
        server.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

3.UDP客户端

1.创建一个DatagramSocket对象,同时指定服务器ip和端口号

2.从控制台获取到要发送的信息,转成字节,封装到DatagramPacket对象,使用send()发送到指定的ip和端口号

3.通过DatagramPacket对象获取到服务器反馈的消息,并且使用receive()填充

4.展示服务器反馈的信息

public class Client {
    private DatagramSocket socket;
    //需要指定服务器的ip和端口号
    private String serverIp ;
    private int serverPort ;

    public Client(String serverIp, int serverPort) throws SocketException {
        this.socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }

    public void start() throws IOException {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 从控制台读取要发送的数据
            System.out.print("> ");
            String request = scanner.next();
            if (request.equals("exit")) {
                System.out.println("客户端退出");
                break;
            }
            // 2. 构造UDP请求,并发送
            //上面的IP地址是一个字符串,需要使用InetAddress.getByName来转换成一个整数.
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);
            //3. 读取从服务器返回的响应,并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            // 4. 把解析好的结果显示出来
            System.out.println("服务器反馈的消息:"+response);
        }
    }

    public static void main(String[] args) throws IOException {
        //ip:本地ip
        //端口:服务器端口号
        Client client = new Client("127.0.0.1", 9090);
        client.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

效果:

UDP

4.UDP编程执行顺序

  • 启动服务器, 执行socket.receive(requestPacket)的时候,发现客户端还没有发送消息,就阻塞等待
  • 启动客户端,从控制台获取到要发送的信息,通过send()进行发送
  • 客户端执行到receive(),发现服务器还没有反馈信息,就阻塞等待;同时服务器接收到了客户端发送来的消息,就一直往后执行,并且通过send()发送反馈消息
  • 客户端接收到服务器的反馈消息后,就正常执行程序
  • 进入下一次循环,依然是服务器先阻塞等待,然后客户端发送了信息后,阻塞等待服务器的反馈消息

5.UDP实战

​ 服务器中的process方法就是用来处理客户端接收到的消息,那么我们就可以再写一个类,继承服务器这个类并且重写process这个方法,实现我们想要得效果。

模拟实现英文单词翻译器(Server和Client使用前面所写的)

服务器:

public class UdpServer extends Server {
    private Map<String, String> dict = new HashMap<>();

    public UdpServer(int port) throws SocketException {
        super(port);
        dict.put("dog", "小狗");
        dict.put("cat", "小猫");
        dict.put("student", "学生");
        dict.put("teacher", "教师");
    }

    @Override
    public String process(String request) {
        return dict.getOrDefault(request, "该单词没有查到");
    }

    public static void main(String[] args) throws IOException {
        Server udpServer = new UdpServer(9090);
        udpServer.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

效果:

GIF 2023-4-8 20-23-41

三、TCP编程

1.TCP套接字

ServerSocket是创建TCP服务端Socket的API

构造方法解释
ServerSocket(int port)创建一个服务端流套接字Socket,并绑定到指定端口
方法解释
Socket accept()开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close()关闭此套接字

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。

不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

构造方法解释
Socket(String host, int port)创建一个客户端流套接字Socket,并与对应IP的主机上,对应端口的进程建立连接
方法解释
InetAddress getInetAddress()返回套接字所连接的地址
InputStream getInputStream()返回此套接字的输入流
OutputStream getOutputStream()返回此套接字的输出流

TCP中的长短连接

TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接:

短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。

长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。

2.TCP服务器

1.创建一个ServerSocket对象,并指定端口号,用来和客户端建立连接

2.当有客户端连接到服务器的时候,通过accept()方法建立与客户端的连接

3.使用inputStream、scanner接收服务器传过来的信息

4.使用outputStream、printWriter处理反馈信息

5.特别注意:与客户端建立连接产生的clientSocket一定要关闭,不然可能造成资源泄露

public class TcpEchoServer {
    //服务器专用的socket,用来和客户端建立连接
    private ServerSocket serverSocket = null;

    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    //启动服务器
    public void start() throws IOException {
        //创建线程池
  		ExecutorService threadPool = Executors.newCachedThreadPool();
        System.out.println("启动服务器!");
        while (true) {
            //和客户端建立连接
            Socket clientSocket = serverSocket.accept();
            //和客户端进行交互,
            //如果直接调用processConnection,会导致accept不及时
            
            //1.使用多线程解决问题
            Thread t = new Thread(() -> {
                processConnection(clientSocket);
            });
            t.start();
            //2.使用线程池解决问题
           /* threadPool.submit(() -> {
                processConnection(clientSocket);
            });*/
        }
    }

    //处理客户端连接
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        //基于clientSocket对象和客户端进行通信
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            //Scanner、PrintWriter都是字符流,避免了直接使用字节流需要我们去判断那个是结束标记
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            //客户端可能有多个请求,所以使用循环来处理
            while (true) {
                // 1. 读取请求
                if (!scanner.hasNext()) {
                    //hasNext()方法判断输入是否还有下一个输入项,若有,返回true,反之false.
                    //当客户端那边关闭了连接,输入源也就结束了,没有了下一个数据,说明读完了,此时hasNext()就为false了
                    System.out.printf("[%s:%d] 客户端下线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                //next 是一直读取到换行符/空格/其他空白符结束,但是最终返回结果里不包含空白符.
                String request = scanner.next();
                // 2. 根据请求构造响应
                String response = process(request);
                // 3. 返回响应的结果,写入网卡
                printWriter.println(response);
                //刷新缓冲区,保证当前写入的数据发送出去
                printWriter.flush();
                System.out.printf("[%s:%d] req: %s; resp: %s \n", clientSocket.getInetAddress().toString(),
                        clientSocket.getPort(), request, response);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                //释放资源
                clientSocket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public String process(String request) {
        System.out.println("客户端发送的消息:" + request);
        String response = "服务器已接收到信息";
        return response;
    }

    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83

3.TCP客户端

1.创建Socket对象,指定服务器的ip和端口号,建立连接

2.使用OutputStream、PrintWriter向服务器传输信息

3.使用InputStream、respScanner接收服务器反馈的信息

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // 与服务器建立TCP连接
        socket = new Socket(serverIp, serverPort);
    }

    public void start() {
        System.out.println("客户端启动!");
        Scanner scanner = new Scanner(System.in);
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            Scanner respScanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                // 1.从键盘上读取用户输入的内容
                System.out.print("> ");
                String request = scanner.next();
                if (request.equals("exit")) {
                    System.out.println("客户端退出");
                    break;
                }
                // 2. 构造请求并发送给服务器
                printWriter.println(request);
                printWriter.flush();

                // 3. 读取服务器的响应
                String response = respScanner.next();
                // 4. 把响应内容显示到屏幕上
                System.out.printf("req: %s; resp: %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", 9090);
        client.start();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

效果:创建一个服务器,两个客户端演示

GIF 2023-4-9 10-18-57

4.TCP编程执行顺序

  • 启动服务器,因为还没有客户端与服务器建立连接,所以执行到accept()时,发生阻塞等待
  • 启动客户端,通过服务器的ip和端口与服务器建立了连接,这个时候,服务器就会正常执行accept()
  • 服务器执行了accept()之后,会尝试从客户端读取请求,但是客户端还没有请求,发生阻塞等待;客户端将会从控制台获取请求,也会发生阻塞
  • 当我们正式开始在控制台输入信息的时候,客户端才能发送请求,然后在读取服务器反馈信息的时候,再次阻塞,等待服务器的反馈信息
  • 服务器读取到请求后,处理信息并给出反馈信息
  • 进入下一次循环,依然是服务器先阻塞等待,然后客户端发送了信息后,阻塞等待服务器的反馈消息
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/84483
推荐阅读
相关标签
  

闽ICP备14008679号