计算机网络——基于UDP与TCP网络编程

  • Post author:
  • Post category:其他



目录


一、什么是UDP与TCP


二、什么是Socket


三、UDP网络编程简单实现


1、DatagramSocket API


2、DatagramPacket API


3、基本使用方法:


我们以一个简易的翻译器的案例,来实现简单的UDP编程:


服务器端代码:


部分代码的说明:


客户端代码:


部分代码说明:


测试结果:


四、TCP网络编程简单实现


ServerSocket API


Socket API


接下来以一个简单的回显服务器来说明TCP编程。


1、建立服务器端


部分代码说明:


2.建立客户端


测试结果:


一、什么是UDP与TCP

TCP与UDP是计算机网络中五层模型的运输层协议。


UDP协议

:(User Datagram Protocol)是一种数据报文协议,它是

无连接协议



不保证可 靠传输

。 因为UDP协议在通信前

不需要建立连接

,因此它的传输效率比TCP高


TCP 协议

:是传输控制协议,它是面向连接的协议,

支持可靠传输和双向通信

。它在传输数据之前需要

先建立连接

,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。


对比来看两个协议的特点:


TCP

:有连接;可靠传输;面向字节流;全双工


UDP

:无连接;不可靠传输;面向数据报;全双工


有连接

:比如打电话,得先接通,才能相互交互数据


无连接

:想发微信,不需要接通,直接就能发数据


可靠传输

:传输过程中,发送方

知道

接受方有没有收到数据


不可靠传输

:传输过程中,发送方

不知道

接收方有没有收到数据


面向字节流

:依字节为单位进行传输(非常类似于文件操作中的字节流)


面向数据报

:以数据报为单位进行传输(一个数据报都会明确大小)一次发送/接受必须是一个完整的数据报,不能是半个或者一个半


全双工

:一条链路,双向通信(双行道)


半双工

:一条链路,单向通信(单行道)



二、什么是Socket

在我们进行网络编程时,都会接触到一个名叫Scoket的概念,应用程序通过Scoket来建立远程连接,Socket通过内部封装好的协议把数据传输到网络。

网络编程套接字,是操作系统给应用程序提供的一组API(叫做socket API)

为什么需要Socket 进行网络通信?因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果 只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket 接口,每个应用程序需 要各自对应到不同的Socket,数据包才能根据 Socket正确地发到对应的应用程序。

socket可以视为应用层和传输层之间的通信桥梁。

使用Socket进行网络编程时,

本质上就是两个进程之间的网络通信

。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收,因此,当Socket 连接成功地在服务器端和客户端之间建立后:对服务器端来说,它的Socket是指定的IP地址和指定的端口号。



三、UDP网络编程简单实现

UDP和TCP相比,就简单的一点,因为UDP不需要建立连接,也就没有区分哪一个是客户端哪一个是服务器端,是依靠数据包来进行实现的,数据包也是收一个发一个,不存在使用流的概念。

UDP也是需要使用Socket来监听端口的的,不过它使用的是java提供的DatagramSocket。

1、DatagramSocket API

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

656fea13032942d2803b376047622ce5.png

2、DatagramPacket API


DatagramPacket

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

ccb9d65877df497b9f3cbbf9f7d8d641.png


注:构造UDP发送的数据时,需要传入


SocketAddress


,该对象可以使用


InetSocketAddress


来创建。

d7f909d80197436a9ffa28a0e05c3638.png

3、基本使用方法:

服务端:

  • 1.创建一个DatagramSocket对象,创建的同时关联一个端口号
  • 2.读取请求,并解析
  • 3.根据请求计算响应
  • 4.把响应写回到客户端
  • 5.打印日志

客户端:

  • 1.创建一个DatagramSocket对象,创建的同时指定服务器的ip和端口号
  • 2.读取输入的数据
  • 3.构造请求并发送给服务器
  • 4.从服务器读取响应
  • 5.把数据显示给用户

我们以一个简易的翻译器的案例,来实现简单的UDP编程:

服务器端代码:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;


public class UdpEchoServer {
    //创一个DatagramSocket对象中
    private DatagramSocket socket = null;

    //翻译就是从key——>value的过程
    private Map<String,String> dict = new HashMap<>();

    //构造方法:
    //参数的端口表示我们的服务器要绑定的端口
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);

        //这里利用HashMap存放大量的key,value值
        dict.put("玫瑰","rose");
        dict.put("许愿","Wishing");
        dict.put("不期而遇",
                "unexpected encounters ");
    }

    //启动服务器
    public void start() throws IOException {
        System.out.println("服务器启动!");
        //UDP不需要建立连接,直接接收从客户端传来的数据
        //死循环:不断接收客户端的连接
        while(true) {
            //每循环一次,处理一次请求

            //1、读取请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
            //接收
            socket.receive(requestPacket);
            //把这个datagramPacket对象转成字符串,方便打印
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());

            //2、根据请求计算响应
            String response = process(request);

            //3、把响应写回到客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length
            ,requestPacket.getSocketAddress());
            //写回
            socket.send(responsePacket);

            //4、打印一个日志,记录当前的情况
            System.out.printf("[%s:%d] req:%s;resp:%s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }
    public String process(String Data) {
        return dict.getOrDefault(Data,"词典中未找到");
    }

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


部分代码的说明:


在构造方法中,port参数的是什么意思:

port参数,就是表示该服务器要绑定的端口,目的在于能够让客户端明确是访问主机上的哪个进程,通过端口确定一个进程,就需要在这个进程启动的时候绑定一个端口,并且一个端口只能被一个进程绑定(一般)


为什么使用死循环:

服务器是不知道客户端啥时候发送请求,所以需要时刻准备着,接收


receive() 方法说明:

这个方法的参数,是个输出型参数,调用receive的时候,就需要构造一个空的 DatagramPacket对象,然后把对象交给receive,在receive里面负责把从网卡读到的数据,给填充到这个对象里面

注:构造对象时,里面的new byte[4096],这个是自己调整大小,不要太小就好了


两次构造对象的区别:

512d19e1b469418683b64afd5382a26e.png


requestPacket.getSocketAddress()的作用:

94093b622e9e4337a850f3bc910c5aea.png

记录客户端的IP和端口号

例如:

我买了一个快递,现在想要退货,就需要把这个包裹发回给商家,商家的收货地址和收件人都是在我收到的包裹上写着的。


日志打印:

4bdbdf4b251a4d27bcf2ba4ac764d4b1.png


客户端代码:

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


public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIP;
    private int serverPort;

    //构造方法:
    public UdpEchoClient(String serverIP,int serverPort) throws SocketException {
        //自动让系统指定一个空闲的窗口
        socket = new DatagramSocket();

        this.serverIP = serverIP;
        this.serverPort = serverPort;
    }

    //客户端
    public void start() throws IOException {
        Scanner sc = new Scanner(System.in);
        while(true) {
            //1、从控制台读取用户输入的内容
            System.out.println("->");
            String request = sc.next();

            //2、构造一个UDP请求,发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(this.serverIP),this.serverPort);
            //发送
            socket.send(requestPacket);

            //3、从服务器读取UDP响应数据,并解析
            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 {
        UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
        client.start();
    }
}


部分代码说明:


构造方法的参数:

public UdpEchoClient(String serverIP,int serverPort) {}

前者是服务器IP,后者是服务器端口

疑问?为什么服务器那边,只是端口号,因为服务器一般情况下,IP就是本机IP


为什么让系统自动指定窗口?

socket = new DatagramSocket();

一般情况下,都是系统自动指定的,如果是手动指定,刚好所指定的窗口正在被别人使用,就会引来不必要的麻烦

举例:

我们去餐厅吃饭,每次去我们都喜欢坐在一个拐角处(指定窗口),突然有一天,这里被别人坐了,我们总不能去赶走人家吧,最好还是做个空位做下就好啦,所以通常都是有系统自动指定一个空闲的窗口


关于DatagramPacket构造:


整体的运行流程:

68a105caf63d45b081447c3b432d01a6.png

测试结果:

99478d8d342149a2ba81b5c2e7601ca0.gif



四、TCP网络编程简单实现


TCP的编程实现客户端与服务器端交互,是通过建立连接,从而利用“流”来进行数据交换的,即使用字节输入/输出流,把要发送的数据,存储到字节数组,通过“流”来实现发送与接收。

ServerSocket API

26d44e259a1447eaa3a517d36ae5a506.png

Socket API

16b59e3aad9d41f98a7e68b22fe77683.png

接下来以一个简单的回显服务器来说明TCP编程。

1、建立服务器端

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 TcpEchoServer {
    //代码中会涉及到多个socket对象,使用不同的名字来区分
    private ServerSocket listenSocket = null;

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

    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService service = Executors.newCachedThreadPool();
        while(true) {
            //1、先调用accept来接受客户端的连接
            //如果当前没有客户端来建立连接,accept就会阻塞
            Socket clientSocket = listenSocket.accept();
            //2、再处理这个连接,这里应该要使用多线程,每个客户端上来都分配一个新的线程负责处理


            service.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        processConnection(clientSocket);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
        }
    }

    private  void processConnection(Socket clientSocket) throws IOException {
        System.out.printf("[%s:%d 客户端上线!\n",clientSocket.getInetAddress().toString(),clientSocket.getPort() );
        //接下来处理客户端的请求
        try(InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()) {
            while(true) {
                //1、读取请求并解析
                Scanner sc = new Scanner(inputStream);
                if(!sc.hasNext()) {
                    //读完了,连接断开了
                    System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress().toString());
                    break;
                }
                String request = sc.next();
                //2、根据请求计算响应
                String response = process(request);
                //3、把响应写回给客户端
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                //刷新缓冲区确保数据确实是通过网卡发送出去了
                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 {
            //这个关闭socket
            clientSocket.close();

        }
    }

    public String process(String request) {
        return request;
    }

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

部分代码说明:


为什么使用线程池?


为了实现一个服务器能够并发响应多个客户端的请求,这里引入多线程的方法

1,因为listen()监听函数过后,服务器的ip与端口就会暴露在网络中,网络中连接的各个客户端就可以连接该服务器,而所有的连接请求都会存储在监听文件描述符对应的读缓冲区中,每执行一次accept,就会从该监听文件描述符对应的读缓冲区中读取一个连接,因此,如果是多线程服务器,应该在主线程中将accept函数包含在一个while(true)循环中,让主线程不断从该缓冲区中接收连接。

2,当accept函数执行完以后,就要有对应的子线程处理accept函数返回的客户端,因此,在while循环内部,每当执行完accept成功以后,就创建一个子线程,让该线程去处理该客户端。子线程内部的流程就是与客户端互相交流的一些代码。


为什么关闭socket

socket也是一个文件,一个进程能够同时打开的文件个数有上限(PCB文件描述符表是有限的),listenSocket对象在TCP服务器程序中,只有一个唯一的对象,一般不会把文件描述符表占满(随着进程的结束,自动释放)。而clientSocket是在死循环里面的,每次来一个客户端,建立连接,都要分配一个,这个对象就会被反复创建按销毁实例,每创建一个,都要销毁一个文件描述符,因此需要把不再使用的clientSocket及时释放掉


2.建立客户端

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 = null;

    public TcpEchoClient(String serverIP,int serverPort) throws IOException {
        //和服务器建立连接,就需要知道服务器在哪儿了
        //这里和UDP客户端差别比较大
        socket = new Socket(serverIP,serverPort);
    }
    public void start() {
        Scanner sc = new Scanner(System.in);
        try(InputStream inputStream = socket.getInputStream();
            OutputStream outputStream = socket.getOutputStream()) {
            while (true) {
                //1、从控制台读取数据,构成一个请求
                System.out.println("->");
                String request = sc.next();
                //2、发送请求给服务器
                PrintWriter printWriter = new PrintWriter(outputStream);
                printWriter.println(request);
                //这个flush不要忘记,否则可能导致请求没有真发出去
                printWriter.flush();
                //3、从服务器读取响应
                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();
                //4、把响应显示到界面上
                System.out.println(response);

            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

测试结果:

f9feed92f3de4cd382e27307957931d1.gif

下期见啦!!!


438e6dfc4f47434582d73d669c2c7fbe.gif



版权声明:本文为LYJbao原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。