网络编程基础

目录

♫什么是网络编程

 ♫Socket套接字

♪什么是Socket套接字

♪数据报套接字

♪流套接字

 ♫数据报套接字通信模型

♪数据报套接字通讯模型

♪DatagramSocket

♪DatagramPacket

♪实现UDP的服务端代码

 ♪实现UDP的客户端代码

♫流套接字通信模型

♪流套接字通讯模型

♪ServerSocket

♪Socket

♪实现TCP的服务端代码

♪实现TCP的客户端代码


♫什么是网络编程

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络数据传输。在网络编程中,获取一个网络资源,涉及到两次网络数据传输: 发送端请求数据的发送 和 接受端响应数据的发送,其中提供服务的一端叫做接受端,获取服务的一端叫做客户端。

 ♫Socket套接字

♪什么是Socket套接字

Socket套接字是由操作系统提供用于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元,是对TCP/IP协议栈的封装(尤其是对传输层的TCP和UDP协议的封装),所以当应用程序使用Socket进行通信时,实际上是在使用TCP或UDP协议进行数据传输。基于 Socket 套接字的网络程序开发就是网络编程。

Socket套接字主要针对传输层协议划分为流套接字和数据报套接字。

♪数据报套接字

数据报套接字是使用传输层 UDP,即 User Datagram Protocol(用户数据报协议),传输层协议。 以下为UDP的特点:

♩.无连接:通信双方在发送数据之前不需要建立连接

♩.不可靠传输:不保证数据包的可靠传输,即数据包可能会丢失、重复或乱序到达接收方

♩.面向数据报:UDP将应用层的数据作为独立的报文进行处理,每个报文都被封装在一个UDP数据报中

♩.全双工:通信双方都可以互相进行通信

♩.大小受限:一次最多传输64k

♪流套接字

流套接字是使用传输层 TCP,即 Transmission Control Protocol(传输控制协议),传输层协议。 以下为TCP的特点:

♩.有连接:服务端需和客户端建立连接后才可以进行通信

♩.可靠传输:确认应答和超时重传共同保证

♩.面向字节流:对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收

♩.全双工:通信双方都可以互相进行通信

 ♫数据报套接字通信模型

♪数据报套接字通讯模型

对于 UDP 协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报,一次接收全部的数据报。 java中使用 UDP 协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用 DatagramPacket 作为发送或接收的 UDP 数据报。对于发送及接收UDP数据报的流程如下:

网络编程基础_第1张图片

♪DatagramSocket

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

 ♩.DatagramSocket的构造方法

构造方法 描述
DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 (一般用于客户端)
DatagramSocket(int port) 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用 于服务端)

 ♩.DatagramSocket的方法

方法 描述
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送)
void close()

关闭此数据报套接字

♪DatagramPacket

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

 ♩.DatagramPacket的构造方法

构造方法 描述
DatagramPacket(byte[] buf, int length) 构造一个DatagramPacket以用来接收数据报,接收的数据保存在 字节数组(第一个参数buf)中,接收指定长度(第二个参数 length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造一个DatagramPacket以用来发送数据报,发送的数据为字节 数组(第一个参数buf)中,从0到指定长度(第二个参数 length)。address指定目的主机的IP和端口号

 ♩.DatagramPacket的方法

方法 描述
InetAddress getAddress() 从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址

int getPort()

从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号

byte[] getData() 获取数据报中的数据

♪实现UDP的服务端代码

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

// UDP 版本的回显服务器
class UdpEchoServer {
    // 网络编程, 本质上是要操作网卡.
    // 但是网卡不方便直接操作. 在操作系统内核中, 使用了一种特殊的叫做 "socket" 这样的文件来抽象表示网卡.
    // 因此进行网络通信, 势必需要先有一个 socket 对象.
    private DatagramSocket socket = null;

    // 对于服务器来说, 创建 socket 对象的同时, 要让他绑定上一个具体的端口号.
    // 服务器一定要关联上一个具体的端口的!!!
    // 服务器是网络传输中, 被动的一方. 如果是操作系统随机分配的端口, 此时客户端就不知道这个端口是啥了, 也就无法进行通信了!!!
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        // 服务器不是只给一个客户端提供服务就完了. 需要服务很多客户端.
        while (true) {
            // 只要有客户端过来, 就可以提供服务.
            // 1. 读取客户端发来的请求是啥.
            //    receive 方法的参数是一个输出型参数, 需要先构造好个空白的 DatagramPacket 对象. 交给 receive 来进行填充.
            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. 把响应写回到客户端. send 的参数也是 DatagramPacket. 需要把这个 Packet 对象构造好.
            //    此处构造的响应对象, 不能是用空的字节数组构造了, 而是要使用响应数据来构造.
            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 request) {
        return request;
    }

    public static void main(String[] args) throws IOException {
        // 端口号的指定, 大家可以随便指定.
        // 1024 -> 65535 这个范围里随便挑个数字就行了.
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

 ♪实现UDP的客户端代码

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

// UDP 版本的 回显客户端
public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp = null;
    private int serverPort = 0;

    // 一次通信, 需要有两个 ip, 两个端口.
    // 客户端的 ip 是 127.0.0.1 已知.
    // 客户端的 port 是系统自动分配的.
    // 服务器 ip 和 端口 也需要告诉客户端. 才能顺利把消息发个服务器.
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        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("goodbye");
                break;
            }
            // 2. 构造成 UDP 请求, 并发送
            //    构造这个 Packet 的时候, 需要把 serverIp 和 port 都传入过来. 但是此处 IP 地址需要填写的是一个 32位的整数形式.
            //    上述的 IP 地址是一个字符串. 需要使用 InetAddress.getByName 来进行一个转换.
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(serverIp), 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);
        // UdpEchoClient client = new UdpEchoClient("42.192.83.143", 9090);
        client.start();
    }
}

当我们分别启动客户端和服务器代码后,就可以进行本机上 UDP 版本的网络通信了:

网络编程基础_第2张图片

注:若想开启多个客户端可以通过 Alt+U 修改配置使 idea 允许创建多个实例

♫流套接字通信模型

♪流套接字通讯模型

对于 UDP 协议来说,具有有连接,面向字节流的特征,即每次都是需要建立连接,并且以流的方式发送数据。 java中使用 TCP 协议通信,主要基于 ServerSocket 类来接收连接,通过 Socket 类使用流的方式来收发数据。对于发送及接收 TCP 字节流的流程如下:

网络编程基础_第3张图片

♪ServerSocket

ServerSocket 是创建TCP服务端Socket的API,可以接受来自客户端的连接请求。

 ♩.ServerSocket的构造方法

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

♪Socket

Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端 Socket。 不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。

构造方法 描述
ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口

♪实现TCP的服务端代码

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;

public class TcpEchoServer {
    private ServerSocket serverSocket = null;

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

    public void start() throws IOException {
        System.out.println("启动服务器");
        while (true) {
            // 使用这个 clientSocket 和具体的客户端进行交流.
            Socket clientSocket = serverSocket.accept();
            processConnection(clientSocket);
        }
    }

    // 使用这个方法来处理一个连接.
    // 这一个连接对应到一个客户端. 但是这里可能会涉及到多次交互.
    private void processConnection(Socket clientSocket) {
        System.out.printf("[%s:%d] 客户端上线!\n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
        // 基于上述 socket 对象和客户端进行通信
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            // 由于要处理多个请求和响应, 也是使用循环来进行.
            while (true) {
                // 1. 读取请求
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNext()) {
                    // 没有下个数据, 说明读完了. (客户端关闭了连接)
                    System.out.printf("[%s:%d] 客户端下线! \n", clientSocket.getInetAddress().toString(), clientSocket.getPort());
                    break;
                }
                // 注意!! 此处使用 next 是一直读取到换行符/空格/其他空白符结束, 但是最终返回结果里不包含上述 空白符 .
                String request = scanner.next();
                // 2. 根据请求构造响应
                String response = process(request);
                // 3. 返回响应结果.
                //    OutputStream 没有 write String 这样的功能. 可以把 String 里的字节数组拿出来, 进行写入;
                //    也可以用字符流来转换一下.
                PrintWriter printWriter = new PrintWriter(outputStream);
                // 此处使用 println 来写入. 让结果中带有一个 \n 换行. 方便对端来接收解析.
                printWriter.println(response);
                // flush 用来刷新缓冲区, 保证当前写入的数据, 雀食是发送出去了.
                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 {
            // 更合适的做法, 是把 close 放到 finally 里面, 保证一定能够执行到!!
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

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

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

♪实现TCP的客户端代码

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 {
    private Socket socket = null;

    public TcpEchoClient(String serverIp, int serverPort) throws IOException {
        // Socket 构造方法, 能够识别 点分十进制格式的 IP 地址. 比 DatagramPacket 更方便.
        // new 这个对象的同时, 就会进行 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()) {
            while (true) {
                // 1. 先从键盘上读取用户输入的内容
                System.out.print("> ");
                String request = scanner.next();
                if (request.equals("exit")) {
                    System.out.println("goodbye");
                    break;
                }
                // 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) {
            e.printStackTrace();
        }
    }

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

分别运行客户端和服务器代码后就可以进行本机上 TCP 版本的网络通信了:

网络编程基础_第4张图片

但是我们上面写的服务端代码如果与一个客户端建立连接后进入 processConnection 方法的 while 循环中,只要该客户端不下线,我们的就无法通过 accept 方法与第二个客户端建立连接,即当前服务器一次只能和一个客户端进行网络通信。要想让服务器能够同时和多个客户端进行通信,就需要引入多线程:

让服务器每建立一个连接就创建一个新线程与之通信,这里直接使用线程池的方式,避免线程频繁的创建销毁造成额外开销:

    public void start() throws IOException {
        System.out.println("服务器启动!");
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true) {
            Socket clientSocket = serverSocket.accept();
            threadPool.submit(() -> {
                processCoonection(clientSocket);
            });
        }
    }

这样子服务器就能同时和多个客户端进行 TCP 通信啦~

你可能感兴趣的:(网络)