基于 Socket 的网络编程

简介: 基于 Socket 的网络编程

网络编程

指网络上的主机, 通过不同的进程, 以编程的方式实现 网络通信 (或称为网络数据传输) (同一台主机的不同进程间, 基于网路的通信也可以称为网络编程)


服务端 & 客户端

网络编程中的概念

服务端: 网络通信中, 提供服务的一方 (进程)

客户端: 网络通信中, 获取服务的一方 (进程)


Socket 套接字

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


Socket 的分类

Socket 套接字根据 针对的传输层协议 可以分为三类:

  1. 流套接字 – 使用传输层 TCP 协议
  2. 数据报套接字 – 使用传输层 UDP 协议
  3. 原始套接字 – 使用自定义传输层协议

TCP & UDP

TCP (Transmission Control Protocol) 传输控制协议 (传输层协议), 特点:

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

UDP (User Datagram Protocol) 用户数据报协议 (传输层协议), 特点:

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

TCP 中的长短连接

短连接: 每次接收到数据并返回响应后, 都会关闭连接. (短连接只能一次收发数据)

长连接: 不关闭连接, 一直保持连接状态, 双方不停的收发数据. (长连接可以多次收发数据)

长短连接拥有不同的特点 :

建立连接, 关闭连接都需要消耗资源, 因此长连接效率更高

短连接一般是客户端主动向服务端发送请求.

而长连接可以是客户端主动向服务端发送请求, 也可以是服务端主动向客户端推送消息

短连接适用于客户端请求频率不高的场景, eg: 浏览网页.

长连接适用于客户端和服务器通信频繁的场景, eg: 聊天室, 实时游戏

BIO & NIO

长连接有两种实现方式, 基于 BIO 的长连接和基于 NIO 的长连接

  • BIO (同步阻塞 IO) : 基于 BIO 的长连接会一直占用系统资源. 在并发情况下, 每个连接都需要阻塞等待, 接收数据. 即每个连接在一个线程中运行, 消耗极大.

BIO & NIO

长连接有两种实现方式, 基于 BIO 的长连接和基于 NIO 的长连接

  • BIO (同步阻塞 IO) : 基于 BIO 的长连接会一直占用系统资源. 在并发情况下, 每个连接都需要阻塞等待, 接收数据. 即每个连接在一个线程中运行, 消耗极大.
  • 如果使用 小众协议 / 自定义协议, 这个动作称为 序列化 (一般是将对象转换成特定数据格式)

对于 接收数据 时的 数解析动作 来说

  • 如果使用 知名协议, 这个动作称为 分用
  • 如果使用 小众协议 / 自定义协议, 这个动作称为 反序列化 (一般是基于接收数据特定的格式, 转换成程序中的对象 )

协议的设计

一般根据字段的特点进行设计

除此之外, 协议中还会包含: 状态码, 请求类型 等等内容


基于 TCP 的回显服务器设计

服务端

  1. 使用 ServerScoket 创建服务端程序
  1. 调用 ServerSocket.accept() 方法建立连接
  2. 读取请求数据
  3. 根据请求计算响应 (此处为回显服务器, 将请求数据当作响应数据直接返回)
  4. 返回响应数据
package network;

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.nio.charset.StandardCharsets;
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("启动服务器");
        // 此处使用 CachedThreadPool, 使用 FixedThreadPool 不太合适 (线程数量不应该固定)
        ExecutorService threadPool = Executors.newCachedThreadPool();
        while(true) {
            Socket clientSocket = serverSocket.accept();
            threadPool.submit(() -> {
                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 sc = new Scanner(inputStream);
                if(!sc.hasNext()) {
                    // 没有下个数据, 说明读完了. (客户端关闭了连接)
                    System.out.printf("[%s:%d] 客户端下线!\n",
                            clientSocket.getInetAddress(),
                            clientSocket.getPort());
                    break;
                }
                // 注意, 此处使用 next 是一直读取到 换行符/空格/其他空白符 结束, 但是最终结果不包含上述 空白符(因此后面要自己再补一个空白符).
                String request = sc.next();
                // 根据请求构造响应
                String response = process(request);
                // 3.返回响应结果.
                outputStream.write((response+"\n").getBytes(StandardCharsets.UTF_8));
                outputStream.flush();
                //   OutputString 没有 write(String) 这样的功能. 可以把 String 里的字节数组拿出来, 进行写入;
                //   也可以使用 字符流 来转换
//                PrintWriter printWriter = new PrintWriter(outputStream);
                // 此处使用 println 来输出 response, 让结果中带有一个 \n 换行. 方便对端来接收解析.
//                printWriter.println(response);
                // flush 用来刷新缓冲区, 保证当前写入的数据, 一定会发送出去(println 输出后有可能会先放在缓冲区, 等缓冲区满会自动输出)
//                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 {
            // 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();
    }
}

客户端

  1. 使用 Socket 创建客户端程序
  2. 调用 Socket 的构造方法时, 会自动进行 TCP 连接操作
  3. 发送请求数据 (数据这里是由键盘读入)
  4. 接收返回响应数据
package network;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

public class TcpEchoClient {
    private Socket socket = null;

    public TcpEchoClient(String ip, int port) throws IOException {
        // Socket 构造方法, 能够识别 点分十进制 格式的 ip 地址, 比 DatagramPacket 更方便.
        // new 对象的同时, 就会进行 Tcp 连接操作.
        socket = new Socket(ip, port);
    }

    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("bye")) {
                    System.out.println("goodbye");
                    break;
                }
                // 2. 把读取的内容构造成请求, 发送给服务器.
                outputStream.write((request+"\n").getBytes(StandardCharsets.UTF_8));
                outputStream.flush();
//                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();
    }
}

基于 UDP 的回显服务器设计

服务端

  1. 使用 DatagramSocket 创建 Socket 对象, 来建立连接
  2. 读取请求数据 (使用 DatagramPacket 来接收数据 [输出型参数] )
  3. 根据请求计算响应
  4. 返回响应数据
package network;

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

// UDP 版本的回显服务器
public 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] ,0, 4096);
            socket.receive(requestPacket);
            // 此时这个 DatagramPacket 是一个特殊的对象, 但不方便直接进行处理. 可以把这里包含的数据拿出来, 构造成一个字符串, 以便处理
            // requestPacket.getLength(): 是数据内容的长度       requestPacket.getData().length: 是构成 packet 的字节数组的容量
            String request = new String(
                    requestPacket.getData(),
                    0, requestPacket.getLength()); //new String 后面的参数是要数据内容的长度
            // 2. 根据请求计算响应, 由于此处是回显服务器, 相应和请求相同
            String response = process(request);
            // 3. 把响应写回到客户端. send 的参数也是 DatagramPacket. 需要把这个 Packet 对象构造好.
            //    此处构造的响应对象, 不能是用空的字节数组构造了, 而是要使用响应数据来构造.                                        //SocketAddress 里面有 address 和 port (IP和端口号)
            DatagramPacket responsePacket = new DatagramPacket(
                    response.getBytes(), 0 ,
                    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();
    }
}

客户端

  1. 使用 DatagramSocket 来创建 Socket 对象
  2. 创建 DatagramPacket (内含 信息发送到的服务器信息 : ip, port)
  1. 发送请求数据 (DatagramPacket 中的服务器信息, 可以告诉 Socket 对象, “请把我发送给谁”)
  2. 接收响应数据
package network;

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

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

    // 存储的是服务器的 ip 和 端口号, 以便通讯使用
    private String serverIp;
    private int serverPort;

    // 一次通信, 需要有两个 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 sc = new Scanner(System.in);

        while(true) {
            // 1. 从控制台读取要发送的数据
            System.out.println(">");
            String request = sc.next();
            if(request.equals("bye")) {
                System.out.println("goodbye!");
                break;
            }

            // 2. 构造成 UDP 请求, 并发送
            //    构造这个 Packet 的时候, 需要把 severIp 和 port 都传进来. 但是此处 IP 地址需要填写一个 32 位的整数形式
            //    上述的 IP 地址是一个字符串. 需要使用 InetAddress.getByName 来进行一个转换.
            DatagramPacket requestPacket = new DatagramPacket(
                    request.getBytes(), 0, request.getBytes().length,
                    InetAddress.getByName(this.serverIp), this.serverPort);
            socket.send(requestPacket);

            // 3. 读取服务器的 UDP 响应, 并解析
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 0, 4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0 , responsePacket.getLength());

            // 4. 把解析好的结果显示出来
            System.out.printf("[%s:%d] response: %s; request: %s;\n",
                    responsePacket.getAddress().toString(),
                    responsePacket.getPort(), response, request);

        }
    }

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

目录
相关文章
|
4天前
|
Java 数据挖掘 开发者
Java网络编程进阶:Socket通信的高级特性与应用
【6月更文挑战第21天】Java Socket通信是分布式应用的基础,涉及高级特性如多路复用(Selector)和零拷贝,提升效率与响应速度。结合NIO和AIO,适用于高并发场景如游戏服务器和实时数据分析。示例展示了基于NIO的多路复用服务器实现。随着技术发展,WebSockets、HTTP/2、QUIC等新协议正变革网络通信,掌握Socket高级特性为应对未来挑战准备。
|
5天前
|
网络协议 安全 Java
Java网络编程入门涉及TCP/IP协议理解与Socket通信。
【6月更文挑战第21天】Java网络编程入门涉及TCP/IP协议理解与Socket通信。TCP/IP协议包括应用层、传输层、网络层和数据链路层。使用Java的`ServerSocket`和`Socket`类,服务器监听端口,接受客户端连接,而客户端连接指定服务器并交换数据。基础示例展示如何创建服务器和发送消息。进阶可涉及多线程、NIO和安全传输。学习这些基础知识能助你构建网络应用。
14 1
|
18天前
|
网络协议 算法 Linux
【嵌入式软件工程师面经】Linux网络编程Socket
【嵌入式软件工程师面经】Linux网络编程Socket
37 1
|
8天前
|
网络协议 Java API
【Java】Java Socket编程:建立网络连接的基础
【Java】Java Socket编程:建立网络连接的基础
14 1
|
21天前
|
网络协议 Unix API
24.Python 网络编程:socket编程
24.Python 网络编程:socket编程
20 2
|
18小时前
|
Java 机器人 大数据
如何在Java中进行网络编程:Socket与NIO
如何在Java中进行网络编程:Socket与NIO
|
1天前
|
网络协议
逆向学习网络篇:通过Socket建立连接并传输数据
逆向学习网络篇:通过Socket建立连接并传输数据
4 0
|
5天前
|
监控 算法 Java
socket网络编程详解
socket网络编程详解
|
21天前
|
网络协议 API
网络编程套接字(2)——Socket套接字
网络编程套接字(2)——Socket套接字
10 0
|
5天前
|
缓存 监控 Java
Java Socket编程最佳实践:优化客户端-服务器通信性能
【6月更文挑战第21天】Java Socket编程优化涉及识别性能瓶颈,如网络延迟和CPU计算。使用非阻塞I/O(NIO)和多路复用技术提升并发处理能力,减少线程上下文切换。缓存利用可减少I/O操作,异步I/O(AIO)进一步提高效率。持续监控系统性能是关键。通过实践这些策略,开发者能构建高效稳定的通信系统。