网络编程套接字

网络编程

什么是网络编程

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

Socket套接字

网络编程的核心就是 Socket API (操作系统给应用程序提供网络编程的 API)

可以认为是Socket API 是跟传输层密切相关的, 传输层里提供了最核心的两种协议, (UDP, TCP), 因此Socket API 也提供了两种风格(UDP,TCP)

简单认识UDP和TCP
UDP: 无连接, 不可靠传输, 面向数据报, 全双工
TCP: 有连接, 可靠传输, 面向字节流, 全双工
有连接就是类似于打电话这种, 需要建立连接才能通信, 建立连接需要对方接受, 如果连接没有建立好就通信不了

无连接就像是发短信/发微信这种, 直接发就行了
什么是可靠不可靠传输呢?

网络环境非常复杂, 不能保证所发送的数据100%到达

发送方能知道自己发的数据是发过去了, 还是丢了(打电话时=是可靠传输, 发短信发微信是不可靠传输,但是带已读功能的就可以认为是可靠传输), 可靠不可靠跟有连接还是无连接时没有关系的
面向字节流: 数据传输和文件读取类似于"流式"的

面向数据报: 数据传输以一个个"数据报"为基本单位(一个数据报可能是若干个字节, 带有一定的格式)
全双工就是一个通信通道可以双向传输, (既可以发送, 也可以接收)

基于 UDP 编写一个客户端服务器网络通信程序

DatagramSocket

使用这个类来表示 Socket 对象, 在操作系统中, 把这个Socket对象也是当成一个文件来处理, 相当于是文件描述符上的一项

普通的文件, 对应的硬件设备是硬盘,

Socket 文件, 对应的硬件设备是网卡

一个 Socket 对象, 就可以和另外一个主机通信了. 要想和多个不同的主机进行通信就需要创建多个 Socket 对象

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

DatagramSocket 构造方法:

方法签名 方法说明
DatagramSocket() 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(intport) 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)

上述没有参数的版本, 就是没有指定端口, 系统则会自动分配一个空闲的端口

有参数的版本是要传入一个端口, 此时就是让 Socket 对象和这个端口关联起来,

本质上说, 不是进程和端口之间建立联系, 是进程中的 Socket 对象和端口建立了联系

DatagramSocket 方法:

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

receive 这个方法此处传入的相当于一个空的对象, receive 方法内部会对这个空对象进行填充, 从而构造出结果数据, 参数也是一个"输出型参数"

close 这个方法是释放资源的, 用完之后进行关闭

DatagramPacket

DatagramPacket 表示 UDP 传输的一个报文

DatagramPacket 构造方法:

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

编写一个最简单的 UDP 版本的客户端, 服务器程序----回显示服务器(echo server)

一个普通的服务器是: 收到请求, 根据请求计算响应, 返回响应

我们这里省略了计算的过程, 也就是请求什么就返回什么, (这个代码没有什么实际业务, 只是展示一下 Socket API 的用法)

作为一个真正的服务器"根据请求计算响应是及其重要的"

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

// UDP 版本的回显服务器
public class UdpEchoServer {
    // 网络编程, 本质上是要操作网卡
    // 但是网卡不方便直接操作, 在操作系统内核中, 使用"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);
            // receive 内部会针对参数对象进行填充, 填充的数据来自于网卡
            socket.receive(requestPacket);
            // 此时DatagramPacket 是一个特殊的对象. 不好处理, 可以把这里包含的数据拿出来, 构造一个字符串
            // 此处给的最大长度是4096, 但是这里的这些空间不一定用满了, 可能只用了一小部分
            // 因此构造字符串的时候, 就通过getLength 来获取实际的数据报长度,
            // 把这个实际有小部分构造成字符串即可
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());

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

            // 3. 把响应写回客户端, sent 的参数也是DatagramPacket, 需要把Packet对象构造好
            // 此处构造的响应对象, 不能用空的字节数组构造, 需要使用响应数据来构造
            // 获取到客户端的IP和端口号(这两个信息本来就在 requestPacket中)
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);
            // 4. 打印日志, 当前这次请求响应的处理中间结果
            // 参数分别是 Packet 里的IP, 端口
            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();
    }
}

对于我们这个程序, 这里的while是得一直循环的, 这样的死循环在服务器程序中是没啥问题.一般服务器是7*24 小时运行的

DatagramSocket 这个类的 receive 能阻塞, 会因为操作系统里原生提供的API就是阻塞的函数, 这里的阻塞不是Java实现的, 而是操作系统内核实现的.

系统里对于IO操作本身就有这样的阻塞机制, 哪个线程如果进行IO操作, 在IO完成之前, 就会自动把对应的线程放在阻塞队列中, 暂时不参与调度.

UDP 版本的回显客户端

java 复制代码
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;

    // 构造这个 socket 对象, 不需要显示绑定一个端口(系统自动分配)
    // 一次通信需要两个ip, 两个端口
    // 客户端的 ip 是 127.0.0.1
    // 服务器 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);
        client.start();
    }
}


我们在基于上面的代码, 实现一个查词典的功能

java 复制代码
/**
 * @describe
 * @author chenhongfei
 * @version 1.0
 * @date 2023/10/24
 */
package network;

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

// 对于DictServer, 和EchoServer 相比, 大部分东西都是一样的
// 主要是对于根据请求计算响应的代码进行修改
public class UdpDictServer extends UdpEchoServer{
    private Map<String,String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);

        // 给这个dict设置内容
        dict.put("dog","狗");
        dict.put("cat","猫");
        dict.put("hello","你好");
        // .....
    }

    public String process(String request) {
        // 查词典的过程
        return dict.getOrDefault(request,"当前单词没有查询到结果");
    }

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

    }

}

如果我们多个客户端都使用同一个端口, 就会出现异常

基本过程:

  1. 服务器先启动, 运行到 receive 阻塞
  2. 客户端开始执行, 客户端开始读取用户 输入的内容
  3. 客户端发送请求
  4. 客户端在这里 进行阻塞等待服务器响应, 接着服务器接受请求,
  5. 根据请求计算响应
  6. 执行send 返回响应,
  7. 最后客户端从阻塞中返回,读到响应了

TCP流套接字编程

TCP 提供的API主要是两个类

SeverSocket 专门给服务器使用的 Socket 对象

Socket 既可以给客户端使用, 也可以给服务器使用

这在里我们要注意的是: TCP 不需要一个类来表示"TCP 数据报", TCP 不是以数据报为单位来传输的, 是以字节的形式"流式传输"

ServerSocket API

ServerSocket 是创建TCP服务端Socket的API

ServerSocket 构造方法:

方法签名 方法说明
ServerSocket(int port) 创建一个服务端流套接字Socket,并绑定到指定端口

ServerSocket 方法:

方法签名 方法说明
Socketaccept() 开始监听指定端口(创建时绑定的端口),有客户端连接后,返回一个服务端Socket对象,并基于该Socket建立与客户端的连接,否则阻塞等待
void close() 关闭套接字

Socket

在服务器这边, 是由 acecpt 返回的

这客户端这边, 是得由我们构造的, 构造的时候指定一个IP 和 端口号

这里指定的IP 和 端口号 是服务器的IP 和 端口号, 有了这个信心就能和服务器建立连接了

Socket 方法:

方法签名 方法说明
InetAddress getInetAddress() 返回套接字所连接的地址
InputStream getInputStream() 返回此套接字的输入流
OutputStream getOutputStream() 返回此套接字的输出流

TCP 回显服务器

java 复制代码
import java.io.*;
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 {
    private ServerSocket serverSocket = null;

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

    public void start() throws IOException {
        System.out.println("服务器启动 !");
        // 此处使用这个动态变化的线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while (true) {
            // 使用这个clientSocket 和具体的客户端进行交流
            Socket clientSocket = serverSocket.accept();
//            Thread t = new Thread(() -> {
                // 多线程版本
                // 这里最大的问题就是频繁的申请释放资源
//                try {
//                    processConnection(clientSocket);
//                } catch (IOException e) {
//                    throw new RuntimeException(e);
//                }
//            });
//            t.start();
                // 使用线程池来解决
            threadPool.submit(() -> {
                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());
        // 继续上述的Socket对象和客户端通行
        try (InputStream inputStream = clientSocket.getInputStream();
             OutputStream outputStream = clientSocket.getOutputStream()) {
            // 因为要处理多个请求和响应, 也是要循环来进行
            while (true) {
                // 1. 读取目录
                Scanner scanner = new Scanner(inputStream);
                if (!scanner.hasNextInt()) {
                    // 没有数据了, 就说明读完了(客户端关闭连接)
                    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(request);
                // 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 {
            clientSocket.close();
        }
    }
    public String process(String request) {
        return request;
    }

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

这里用到clientSocket , 此时任意一个客户端连接上来, 都会返回或创建一个Socket对象

Socket 就是文件, 每次创建一个clientSocket 对象后, 就会占用一个文件描述符表的位置

所以在使用完之后要进行关闭

TCP 回显客户端

java 复制代码
import java.io.*;
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);
                // 此处加上close 确保数据发送出去
                printWriter.flush();
                // 3. 读取服务器的响应
                Scanner respScanner = new Scanner(inputStream);
                String response = respScanner.next();
                // 4. 把响应的内容显示到界面上
                System.out.println(request);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1",4090);
        client.start();
    }
}
相关推荐
傻啦嘿哟1 小时前
代理IP在后端开发中的应用与后端工程师的角色
网络·网络协议·tcp/ip
Red Red1 小时前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
亚远景aspice3 小时前
ISO 21434标准:汽车网络安全管理的利与弊
网络·web安全·汽车
Estar.Lee3 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
友友马3 小时前
『 Linux 』网络层 - IP协议(一)
linux·网络·tcp/ip
wenchm4 小时前
细说STM32单片机DMA中断收发RTC实时时间并改善其鲁棒性的另一种方法
stm32·单片机·嵌入式硬件
码老白4 小时前
【老白学 Java】Warshipv2.0(二)
java·网络
HackKong5 小时前
小白怎样入门网络安全?
网络·学习·安全·web安全·网络安全·黑客
vmlogin虚拟多登浏览器5 小时前
虚拟浏览器可以应对哪些浏览器安全威胁?
服务器·网络·安全·跨境电商·防关联