JavaEE——网络编程套接字Socket

操作系统为我们实现了传输层及以下的协议,程序猿要做的主要是实现应用层方面的协议,也就是网络编程。

目录

网络编程

Socket套接字

UDP数据报套接字

TCP流套接字


网络编程

网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。即使是同一个主机,只要进程不同,通过网络传输数据也属于网络编程的范围。

学习网络编程,首先需要了解: 

  • 接收端与发送端:数据的接收方进程,即此次通信中的目的IP,称为接收端;数据的发送方进程,即此次通信中的源IP,称为发送端。发送端与接收端都可以简称为收发端。
  • 客户端与服务器:网络传输中提供服务的一端称为服务器。获取服务的一端称为客户端。
  • 请求和响应:客户端发送请求,服务器响应数据并反馈。

假如我去餐馆吃饭,那么我就是客户端,餐馆就是服务器。我要了一份炒饭,这就是请求,餐馆把炒饭做出来并放到我旁边,就是响应。

Socket套接字

Socket套接字,是基于TCP/IP协议的网络通信的基本操作单元,用于网络通信。基于Socket套接字的开发就是网络编程。

Socket套接字针对传输层协议分为三类:

流套接字:使用传输层TCP协议。

数据报套接字:使用传输层UDP协议。

原始套接字:自定义传输层协议,用于读写内核没有处理的IP协议数据。

TCP协议特点

  • 有连接:基于TCP协议实现的程序,必须先连接才能传输(就像打电话,打通了以后才能说话)。
  • 可靠性传输:TCP协议尽可能地保证数据传输,关心对方是否收到。
  • 面向字节流:以一个字节一个字节地传输。
  • 全双工:双向通信,对应半双工(单向传输)。
  • 传输大小不限:每次传输的空间大小不受限制。

UDP协议特点

  • 无连接:不需要连接就可以通信(相当于发短信)。
  • 不可靠传输:发送数据后就不管了,不关心对方是否收到。
  • 面向数据报:每次传输只能以数据报为单位,不能一次传输半个数据报。(假如一个数据报包含了100个字节,那么这100个字节只能一起发送,不能分开发)
  • 全双工:双向通信。
  • 大小受限:每次传输最多传输64kb。

UDP数据报套接字

UDP数据报套接字需要用到DatagramSocket的相关API。

DatagramSocket的核心方法是receive()和send(),同时,需要一个DatagramPacket类,也就是数据报来接收请求或者响应,UDP服务器实现流程是这样的:

  1. 初始化一个服务器对象,这个对象的端口应该手动指定(端口号用来识别不同的服务器,如果自动分配客户端就难以找到相应的服务器了)。
  2. 实现其UDP的特点:面向数据报,也就是要创建一个DatagramPacket类来接收请求(可以把它当作我们去食堂打饭用的盘子),接着在receive方法种传入该数据报实例对象,receive方法会接收datagramPacket中的数据,如果没有数据就阻塞等待,直到客户端输入请求(相当于我们把盘子递给打饭阿姨,阿姨打好饭以后才能把盘子递给我们,否则只能等待)。
  3. 把接收到的请求转换为字符串并计算响应:提取datagramSocket中的所有信息转换成字符串,再调用响应函数计算出响应。
  4. 创建一个新的数据报,并把响应内容传入,最后把数据报通过send函数传给客户端。

当然,只要客户端发起请求,服务器都要响应,因此服务器需要循环的处理响应。

public class UdpEchoServer {
    private DatagramSocket socket;

    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            //创建接收信息的空间
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
            //接收信息
            socket.receive(requestPacket);
            //处理信息
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength(), StandardCharsets.UTF_8);
            String response = process(request);//这里的响应由开发者实现
            //发送信息到客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());//最后一个参数是客户端的地址(请求与响应需要对应)
            socket.send(responsePacket);
            //打印信息
            System.out.printf("[%s: %d] req: %s, res: %s\n",
                    requestPacket.getAddress().toString(), requestPacket.getPort(), request, response);
        }
    }

    public String process(String request) {
        //这里计算响应结果
    }

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

注意这里的字符串应该先转换为byte数组在计算长度,假如字符串中包含汉字等字符,那么得出的长度就不对了。 

与之相对的客户端类似:

客户端不需要直到自己的端口号,因此不必手动指定(客户端的数量很多,如果端口号重复则这些客户端都无法使用),交给系统自动分配即可。但是客户端需要指定服务器的IP和端口号。

客户端流程:输入请求->用数据报接收->send到服务器,使用数据报接收响应结果->使用响应结果

public class UdpClient {
    private final DatagramSocket socket;
    private final String serverIP;
    private final int serverPort;

    public UdpClient(String ip, int port) throws SocketException {
        socket = new DatagramSocket();
        serverIP = ip;
        serverPort = port;
    }
    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("-> ");
            //输入请求
            String request = scanner.next();
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                    request.getBytes().length, InetAddress.getByName(serverIP), serverPort);//最后两个参数用于指定服务器的IP和端口号
            //发送请求到服务器
            socket.send(requestPacket);
            //接收服务器的处理结果
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024], 1024);
            socket.receive(responsePacket);
            //打印信息
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength(), StandardCharsets.UTF_8);
            System.out.printf("req:  %s, res: %s\n", request, response);
        }
    }

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

同样需要注意输入的请求先转换为byte数组再计算长度。

TCP流套接字

TCP流套接字的实现与UDP大不相同,其实从二者的特点就可以看出来。

TCP流套接字不再使用DatagramSocket类,而是使用ServerSocket和Socket类,其中ServerSocket只做一件事,那就是与客户端建立连接,剩下的交给Socket类处理。

TCP服务器实现流程:

  1. 构造服务器对象(指定端口号)
  2. 建立连接:建立连接通过ServerSocket类的accept方法实现,返回Socket,因为服务器需要处理多个客户端的请求,因此需要循环的调用accept方法以便于客户端可以及时连接到服务器。
  3. 读取请求并处理:TCP的特点是面向字节流,因此需要用到io包中的InputStream和OutputStream,通过Scanner读取input Stream中的数据,读取到的数据(也就是客户端发起的请求)交给响应函数处理,当然也需要循环的处理请求。
  4. 返回响应到客户端:利用PrintWriter类的println方法写入到outputStream中,通过flush函数刷新缓冲区,把内容提交给客户端(如果对PrintWriter类的方法不太熟悉的可以看一下:文件操作与IO操作)。

由于客户端与服务器建立了连接,当客户端断开连接时,需要及时的调用close方法关闭连接(虽然不关闭也问题不大,但最好还是关闭,毕竟占用了资源)。

public class TcpEchoServer {
    private final ServerSocket serverSocket;
    public TcpEchoServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动!");
        while (true) {
            //等待客户端连接
            System.out.println("等待客户端连接…");
            Socket clientSocket = serverSocket.accept();
            processConnect(clientSocket);
        }
    }
    private void processConnect(Socket clientSocket) {
        System.out.printf("[%s: %d] 客户端连接成功!\n",
                clientSocket.getInetAddress().toString(), clientSocket.getPort());
        try (InputStream inputStream = clientSocket.getInputStream()) {
            try (OutputStream outputStream = clientSocket.getOutputStream()) {
                Scanner scanner = new Scanner(inputStream);
                //循环处理请求
                while (true) {
                    if (!scanner.hasNextLine()) {
                        //读取完毕
                        System.out.printf("[%s: %d] 客户端断开连接!\n",
                                clientSocket.getInetAddress().toString(), clientSocket.getPort());
                        break;
                    }
                    String request = scanner.nextLine();
                    String response = process(request);
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(response);
                    printWriter.flush();//刷新缓冲区,确保第一时间获取请求
                    System.out.printf("[%s: %d] req: %s, res: %s\n",
                            clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                clientSocket.close();//关闭资源
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    public String process(String request) {
        //这里处理请求返回响应
    }
    public static void main(String[] args) throws IOException {
        TcpEchoServer server = new TcpEchoServer(9090);
        server.start();
    }
}

对应的客户端实现流程:创建客户端实例对象(使用Socket类),构造时需要传入参数:IP和端口号(这里的IP和端口号为连接的服务器IP和端口号)->输入请求->请求通过PrintWriter写入outputStream中->刷新缓冲区,把请求提交给服务器->接收服务器返回的响应:

public class TcpClient {
    private Socket socket;
    public TcpClient(String ServerIP, int serverPort) throws IOException {
        socket = new Socket(ServerIP, serverPort);//相当于打电话的拨号
    }
    public void start() {
        try (InputStream inputStream = socket.getInputStream()) {
            try (OutputStream outputStream = socket.getOutputStream()) {
                System.out.println("连接成功!");
                Scanner scanner = new Scanner(System.in);
                while (true) {
                    //输入请求
                    System.out.print("-> ");
                    String request = scanner.nextLine();
                    //发送请求到服务器
                    PrintWriter printWriter = new PrintWriter(outputStream);
                    printWriter.println(request);
                    printWriter.flush();
                    //接收服务器响应结果
                    Scanner resScanner = new Scanner(inputStream);
                    String response = resScanner.nextLine();
                    //输入结果
                    System.out.printf("req: %s res : %s\n", request, response);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws IOException {
        TcpClient client = new TcpClient("127.0.0.1", 9090);
        client.start();
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_71020872/article/details/130156398