网络编程套接字和 socket

socket 是什么

要介绍网络编程套接字和 soket 首先我们要知道 TCP / IP 5层网络模型,在这个网络模型中我们的应用程序在最上面的应用层;操作系统作用在第二层传传输层和第三层网络层;驱动程序在第四层数据链路层;网络相关的一些硬件,如网卡等,在第五层物理层。 soket 是一种广义的文件,相当于操作系统对网卡一类的硬件进行的扩充,我们对 socket 文件进行操作,实际上就是在对网卡等硬件设备进行操作,网络编程套接字 就是为了方便我们对 socket 文件进行操作,传输层协议提供给应用层使用的API ,也叫作 socket API
传输层协议中,最主要的就是 TCP 和 UDP,这两个协议存在很大的差异,因此两组协议提供的 soket API 也存在很大的差异,这里我会通过两个简单的回显服务代码对两组 API 进行简单的介绍。回显服务指的是服务器不对客户端传过来的数据进行任何操作,直接将客户端的请求数据作为响应,原封不动的传回给客户端。但在实际的客户端、服务器交互中,服务器计算响应是我们主要实现的部分,这里只是介绍 soket API,所以不对服务器功能进行设计,直接使用最简单的回显服务。

UDP回显服务

首先,我们先来看一下 UDP 提供的 soket API 的两个主要的类。

	DatagramSocket;
    DatagramPacket;

这两个类虽然没有写明 UDP ,但我们知道, UDP协议传输数据时,采用面相数据报的方式,而Datagram 就是数据报的意思,这也意味着,使用 UDP 协议进行数据传输时,数据必须是一个一个数据报的形式,我们就可以使用DatagramPacket 这个类提供的方法将数据打包成数据报,用DatagramSocket 中的方法解析数据报中的数据,再对这些数据进行各种操作。下面就通过 UDP 回显服务的服务器以及客户端代码对这组 API 进行简单的介绍。

服务器代码

public class UDPEchoServer {
    private DatagramSocket socket = null;

    public UDPEchoServer(int port) throws SocketException {
        // 1. 由于 socket 对象在创建时存在失败的可能,所以我们需要处理异常
        // 2. 客户端和服务器的交互需要明确彼此的IP地址以及端口号
        // 一般会给服务器指定一个端口号,客户端则采用系统分配的端口
        // 因为服务器的端口不明确,客户端就不知道数据要传到哪里,而客户端传输的数据中会包含自身的IP和端口
        // 并且客户的机器上有哪些端口被占用了是不确定的,系统给客户端分配端口可以避免端口冲突
        // 在构造方法中指定服务器端口号
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        // 在一些步骤中用输出做一些提示
        System.out.println("服务器启动");
        while(true) {
            // 服务器启动后,循环读取数据并进行响应
            // 1. 构造 DatagramPacket 对象时,参数传入字节数组,以及一个数据报的最大长度
            DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
            // 2. socket 调用 receive 方法,将空的 packet 作为参数传入,再将接收到的数据写入到 packet 中
            socket.receive(packet);
            // 3. 将写好数据的 packet 构造成字符串
            // 三个参数分别表示 获取请求数据,开始设置的下标,需要设置的长度
            String request = new String(packet.getData(), 0, packet.getLength());
            // 为了验证服务器响应效果,可以分别打印一下请求字符串和响应字符串
            System.out.println(request);
            // 4. 将请求字符串作为参数传输给计算响应的函数,然后将返回值设置成响应字符串
            String response = process(request);
            System.out.println(response);
            // 5. 对返回的响应数据进行解析,并存入到数据报中
            // 这里的三个参数表示 将字符串转成字节数组,将数据报长度置为字节数组长度,获取客户端的IP以及端口号
            // 需要注意的是长度设置一定是response.getBytes().length 而不能直接使用 response.length()
            // 这两者的区别就在于前者是获取到字符串转换成字节数组后的长度,即字节个数,后者是获取到字符串的长度,即字符个数
            // 在有些情况下,一个字符的长度并不是一个字节,例如一个汉字是两个字节
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length, packet.getSocketAddress());
            // 6. 将构造好的响应数据传回给客户端
            socket.send(responsePacket);
        }
    }

    private String process(String request) {
        // 因为是回显服务,所以直接返回请求数据
        return request;
    }

    public static void main(String[] args) throws IOException {
        // 处理异常 并指定好服务器端口号
        UDPEchoServer echoServer = new UDPEchoServer(9090);
        // 调用start 方法启动服务器
        echoServer.start();
    }
}

客户端代码

public class UDPEchoClient {
    private DatagramSocket socket = null;
    private String ip;
    private int port;
    public UDPEchoClient(String ip, int port) throws SocketException {
        // 无参的构造方法表示使用系统分配的端口
        socket = new DatagramSocket();
        // 通过参数的 ip 和端口指定需要连接的服务器 ip 和端口
        this.ip = ip;
        this.port = port;
    }
    public void start() throws IOException {
        // 通过输入读取客户端请求
        Scanner scanner = new Scanner(System.in);
        System.out.println("客户端启动");
        while(true) {
            String request = scanner.nextLine();
            // 将输入的请求构造成数据报, 将在构造方法中设置好的 ip 和端口传入,表明数据的去向
            DatagramPacket packet = new DatagramPacket(request.getBytes(), 0, request.getBytes().length, InetAddress.getByName(ip), port);
            // 将数据发送给服务器
            socket.send(packet);
            // 接收服务器返回的响应数据
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
            socket.receive(responsePacket);
            // 将服务器返回的数据构造成字符串
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            // 打印出构造好的响应字符串
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        // 由于服务器在本机,所以使用本机 ip “127.0.0.1”, 端口号为服务器代码中指定的端口号
        UDPEchoClient udpEchoClient = new UDPEchoClient("127.0.0.1", 9090);
        udpEchoClient.start();
    }
}

运行效果
在这里插入图片描述
在这里插入图片描述

TCP回显服务

我们同样先看一下 TCP 的 socket API 的两个主要的类。

	ServerSocket;
    Socket;

TCP 协议和 UDP 协议存在很大的差别,因此提供 socke API 也有很大的差别。首先,TCP 是面相字节流传输数据,所以不需要将数据包装成数据报,其次,TCP 传输数据是有连接的,因此,不能像 UDP 那样直接传输或者接收数据,需要先让客户端和服务器建立连接。这里的 ServerSocket 是专门提供给服务器使用的类,它的作用很简单,就是建立客户端和服务器之间的连接,后续的操作都通过 Socket 完成。

服务器代码

public class TCPEchoServer {
    private ServerSocket serverSocket = null;
    private int port;
    // 指定服务器端口
    public TCPEchoServer(int port) throws IOException {
        this.port = port;
        serverSocket = new ServerSocket(port);
    }
    public void start () throws IOException {
        System.out.println("服务器启动");
        while(true) {
            // 循环建立连接,此处需要使用多线程处理客户端请求
            // 如果单线程处理,那在一个客户端与服务器建立连接后,直到这个连接断开之前,服务器都不能与别的客户端建立连接
            // 使用多线程就可以让新的线程去处理请求,而服务器直接进入下一次循环,等待新的客户端建立连接
            // 如果没有客户端进行连接,accept() 就会进入阻塞等待状态。
            Socket clientSocket = serverSocket.accept();
            Thread t = new Thread(()-> {
                // 处理请求
                processConnection(clientSocket);
            });
            // 启动线程
            t.start();
        }
    }

    private void processConnection(Socket clientSocket) {
        System.out.println("客户端建立连接");
        // 由于 TCP 是面相字节流,所以针对 TCP socket 的读写可以直接使用针对文件的字节流读写操作。
        // 这里不再是自己 new 对象,而是直接获取 socket 持有的对象
        try (InputStream inputStream = clientSocket.getInputStream()) {
            // 通过字节流对象的 read 方法读取到的是字节数组
            // 而 Scanner 可以直接将字节流对象中的数据读取成字符串,这样更方便
            Scanner scanner = new Scanner(inputStream);
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                // 循环处理请求
                while(true) {
                    if (!scanner.hasNext()) {
                        // 如果 scanner 没有继续读了,就断开连接
                        System.out.println("服务器断开连接");
                        break;
                    }
                    // 获取请求字符串
                    String request = scanner.next();
                    // 打印请求字符串 做简单验证
                    System.out.println(request);
                    // 处理请求,获取响应
                    String response = process(request);
                    // 使用 PrintWriter 包裹 OutputStream
                    // 这个操作类似上面的 Scanner,可以让我们的写操作更方便
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    // 将响应通过上面包裹的 outputStream 写回 socket 文件
                    printWriter.println(response);
                    // 打印字符串进行简单验证
                    System.out.println(response);
                    // 刷新一下缓存区,让客户端能更快的看到响应数据
                    printWriter.flush();
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

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

客户端代码

public class TCPEchoClient {
    private Socket socket = null;
    public TCPEchoClient(String ip, int port) throws IOException {
        // 这里指定的 IP 和端口号并不是绑定给客户端,而是表示客户端需要连接的服务器的 IP 和端口
        socket = new Socket(ip, port);
    }
    public void start() {
        System.out.println("与服务器连接成功");
        // 接下来的读写操作与服务器类似
        try(InputStream inputStream = socket.getInputStream()) {
            // 读取客户输入的请求
            Scanner scanner = new Scanner(System.in);
            try(OutputStream outputStream = socket.getOutputStream()) {
                while(true) {
                    // 构造请求
                    String request = scanner.next();
                    // 把请求写入 socket 并传输给服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    // 刷新缓冲区,让服务器第一时间看到数据
                    printWriter.flush();
                    // 从服务器读取响应数据
                    Scanner responseScanner = new Scanner(inputStream);
                    // 构造成响应字符串
                    String response = responseScanner.next();
                    // 打印响应
                    System.out.println(response);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

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

运行效果
在这里插入图片描述
在这里插入图片描述

以上就是对网络编程套接字,即socket API 和 socket 的一些简单介绍,如有问题,欢迎指正补充。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值