在这里插入图片描述

🎁个人主页:User_芊芊君子
🎉欢迎大家点赞👍评论📝收藏⭐文章
🔍系列专栏:JavaEE

在这里插入图片描述
在这里插入图片描述

摘要

网络编程的本质,是应用程序借助操作系统提供的 Socket API,通过“网卡”与其他主机通信。本文梳理了Socket的核心概念与两大协议的对比:TCP 有连接、可靠传输、面向字节流;UDP 无连接、不可靠传输、面向数据报,二者均为全双工。在此基础上,分别介绍了 UDP(DatagramSocket / DatagramPacket)和TCP(ServerSocket / Socket)的核心API,并过“回显服务器”和“英译中服务器”两个示例演示了客户端/服务器程序的完整编写流程。

img

一、Socket套接字

1.概念

操作系统给应用程序提供的一组网络编程的API,称为 socket api。

socket是操作系统中一种“特殊的文件”,表示“网卡”硬件设备

Socket api是传输层提供给应用层

2.分类

  • TCP协议*:有连接,可靠传输,面向字节流,全双工*
  • **UDP**协议:无连接,不可靠传输,面向数据报,全双工

连接:抽象的,虚拟的连接,描述通信双方的“关系”。

有连接:先保存对方的关键信息,然后发送数据;

无连接:不需要保存对方的关键信息,直接发送数据 (发送数据的函数里,通过参数,指定对方的ip和端口)

可靠传输:数据发出去之后,关心对方是否收到,如果没收到,补救(效率较低)

不可靠传输:数据发出去之后就不管了

面向字节流:TCP和文件类似,可以一次全发,也可以分次发送

面向数据报:UDP传输以“数据报”为单位,进行传输,一次必须发送/接受一个UDP数据报

全双工:同时进行双向通信

半双工:同一时刻,只能单向通信

二、UDP数据报套接字编程

1.API介绍

DatagramSocket

创建一个重要的对象,就相当于在系统内核里创建了一个socket文件。

创建socket文件,也会占用文件描述符表的一个位置,用完之后要及时关闭,否则可能会出现“文件资源泄露”

DatagramSocket 构造⽅法:

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

DatagramSocket ⽅法:

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

DatagramPacket

表示一个UDP的数据报,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 和端口号

DatagramPacket ⽅法:

方法签名方法说明
InetAddress getAddress()从接收的数据报中,获取发送端主机 IP 地址;或从发送的数据报中,获取接收端主机 IP 地址
int getPort()从接收的数据报中,获取发送主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData()获取数据报中的数据

2.代码示例

编写一个udp客户端服务器程序 (回显服务器echo server)

udp客户端给udp服务器发送一个字符串(用户控制台输入)

udp服务器收到之后,把这个字符串,原封不动的返回给客户端

服务器:

package network.udp;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;

public class UdpEchoServer {
    private DatagramSocket socket = null;
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        //启动服务器
        System.out.println("启动服务器...");
        //服务器需要不断处理客户发来的请求
        while (true){
            //循环一次,处理一次请求
            //1.读取请求并解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(requestPacket);//输出型参数
            //把载荷获取出来,构造成string,方便处理请求
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());
            //2.根据请求构造响应
            String response = process(request);
            //3.把响应返回到客户端
            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(),responsePacket.getPort(),request,response);
        }
    }

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

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);//端口号指定范围:0~65535,小于1024也不使用
        server.start();
    }
}

img

客户端:

package network.udp;

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

public class UdpEchoClient {
    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    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.println("-> ");
            String request = scanner.next();
            //2.构造请求,发送给服务器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName(serverIp),this.serverPort);
            socket.send(requestPacket);
            //3.读取服务器响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(),0, responsePacket.getLength());
            //4.打印出response
            System.out.println(response);
        }
    }

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

img

英译中服务器:只需重写process

package network.udp;

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

public class UdpDictServer extends UdpEchoServer{
    private Map<String,String> dict = new HashMap<>();

    public UdpDictServer(int port) throws SocketException {
        super(port);
        dict.put("hello","你好");
        dict.put("cat","小猫");
        dict.put("dog","你好");
        dict.put("pig","你好");
    }
    //重写process

    @Override
    public String process(String request) {
        //实现英文-->中文
        return dict.getOrDefault(request,"[没有找到该单词]");
    }

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

img

三、TCP流套接字编程

1.API介绍

ServerSocket:给服务器使用的

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

调用close,目的是为了释放资源(文件描述符表,防止文件资源泄露,确定不会使用了再释放,UDP进程结束系统自动释放,TCP得手动调用)

Socket:客户端/服务器

Socket 构造方法

方法签名方法说明
Socket(String host, int port)创建一个客户端流套接字 Socket,并与对应 IP 的主机上,对应端口的进程建立连接

Socket 方法

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

2.代码示例

客户端

package network.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 = new Socket(serverIp,serverPort);
    }
    public void start() throws IOException {
        System.out.println("客户端启动...");

        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()){

            Scanner scannerConsole = new Scanner(System.in);
            Scanner scannerNetwork = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);


            while (true){
                //1.从控制台读取用户输入
                System.out.println("-> ");
                String request = scannerConsole.next();
                //2.构造请求,发给服务器
                printWriter.println( request);
                printWriter.flush();
                //3.从服务器读取响应
                if(!scannerNetwork.hasNext()){
                    break;
                }
                String response = scannerNetwork.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();
    }
}

img

服务器

package network.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;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoSever {
    private ServerSocket serverSocket = null;

    public TcpEchoSever(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服务器启动...");
        //创建线程池,Executors.newCachedThreadPool()没有上限
        ExecutorService service = Executors.newCachedThreadPool();
        while (true) {
            //TCP 每次循环先处理连接,
            Socket socket = serverSocket.accept();//阻塞等待,如果有客户端给服务器“打电话”,accept就直接接通,如果没有,就等待
            //processConnection(socket);//直接调用会存在问题
            //主线程负责accept,等待。创建新线程,调用processConnection,读写处理
//            Thread t = new Thread(() ->{
//                try {
//                    processConnection(socket);
//                } catch (IOException e) {
//                    throw new RuntimeException(e);
//                }
//            });
//            t.start();
            //还可以创建线程池,进一步减少频繁创建销毁线程的开销
            service.submit(() ->{
                try {
                    processConnection(socket);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
        }
    }

    private void processConnection(Socket socket) throws IOException {
        //一个连接中,可能会涉及到多组请求/响应的交互
        System.out.printf("[%s,%d] 客户端上线!\n",socket.getInetAddress().toString(),socket.getPort());
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {

            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while(true){
                //1.读取请求并解析
                if(!scanner.hasNext()){
                    //判断tcp连接是否断开
                    break;
                }
                String request = scanner.next();
                //2.根据请求计算响应
                String response = process(request);
                //3.把响应写回给客户端
                printWriter.println(response);
                printWriter.flush();
                //4.打印日志
                System.out.printf("[%s:%d] req: %s; resp: %s\n",socket.getInetAddress().toString(), socket.getPort(), request, response);
            }

        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                System.out.printf("[%s:%d] 客户端下线!\n",socket.getInetAddress().toString(),socket.getPort());
                socket.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

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

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

img

【注意】

启动多个客户端时,服务器只能感知到一个客户端上线,这时我们就需要用到多线程。主线程负责accept,等待。创建新线程,调用processConnection,读写处理。但是频繁创建销毁线程会消耗资源,影响效率。线程池刚好可以解决这些问题,如上代码。

五、 总结

  1. Socket 是什么:操作系统提供的网络编程API,本质是代表网卡的“特殊文件”,同样占用文件描述符,用完需及时关闭,否则会资源泄露。
  2. TCP vs UDP:TCP 先建立连接、关心对方是否收到、以字节流方式分次读写;UDP不保存对方信息、发送即不管、以完整数据报为单位收发。没有优劣之分,按场景选择。
  3. 编程模型:UDP 用 DatagramSocket 收发 DatagramPacket,无需连接;TCP 服务器用 ServerSocket 监听、accept() 拿到 Socket后像读写文件一样读写字节流。
  4. 设计技巧:回显服务器只需重写 process方法即可扩展为翻译服务器等业务服务器——把“网络通信骨架”和“业务逻辑”解耦,是服务器代码复用的关键思想。

在这里插入图片描述在这里插入图片描述

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐