网络编程—TCP、UDP编程

文章目录

    • 一、网络编程概述
      • 1.什么是网络编程
      • 2.Socket套接字
    • 二、UDP编程
      • 1.UDP套接字
      • 2.UDP服务器
      • 3.UDP客户端
      • 4.UDP编程执行顺序
      • 5.UDP实战
    • 三、TCP编程
      • 1.TCP套接字
      • 2.TCP服务器
      • 3.TCP客户端
      • 4.TCP编程执行顺序

一、网络编程概述

1.什么是网络编程

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

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

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

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

2.Socket套接字

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

系统提供的socket api有两种:

UDP

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

TCP

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

网络编程—TCP、UDP编程_第1张图片

二、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();
    }
}

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();
    }
}

效果:

网络编程—TCP、UDP编程_第2张图片

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();
    }
}

效果:

网络编程—TCP、UDP编程_第3张图片

三、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();
    }
}

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();
    }
}

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

网络编程—TCP、UDP编程_第4张图片

4.TCP编程执行顺序

  • 启动服务器,因为还没有客户端与服务器建立连接,所以执行到accept()时,发生阻塞等待
  • 启动客户端,通过服务器的ip和端口与服务器建立了连接,这个时候,服务器就会正常执行accept()
  • 服务器执行了accept()之后,会尝试从客户端读取请求,但是客户端还没有请求,发生阻塞等待;客户端将会从控制台获取请求,也会发生阻塞
  • 当我们正式开始在控制台输入信息的时候,客户端才能发送请求,然后在读取服务器反馈信息的时候,再次阻塞,等待服务器的反馈信息
  • 服务器读取到请求后,处理信息并给出反馈信息
  • 进入下一次循环,依然是服务器先阻塞等待,然后客户端发送了信息后,阻塞等待服务器的反馈消息

你可能感兴趣的:(网络,udp,tcp/ip)