套接字

什么是socket:

所谓的套接字也就是socket,这个socket和我们编写网络程序又有什么关系呢:

socket是传输层给应用层提供的API的集合,socket就相当于是程序使用网络的接口,而网卡是把数据发出去的硬件,socket就相当于是网卡的一个遥控器

我们网络编程主要涉及的就是传输层以及应用层的交互,我们先决定传输层的哪一个协议,是UDP还是TCP,也就是上文所介绍的两种:

后续的各种层是操作系统根据我们代码中的任务来选择的

发送数据的过程:

Java 程序
   ↓
Java Socket 对象
   ↓
文件描述符 fd
   ↓
进程的文件描述符表
   ↓
操作系统内核中的 Socket 对象
   ↓
TCP / IP 协议栈
   ↓
网卡
   ↓
网络

为什么会有文件描述符表:明明我们是讲socket,为什么会有一个文件描述符表呢,原因就是在Linux中很多东西都被抽象成了“可以像文件一样读写的资源”,所以他们统一通过文件描述符表(fd)来管理,但是我们学习的并不是linux操作系统啊,为什么会有什么个说法,因为在基本上所有的操作系统中,都有这么一个概念,都会让程序拿到一个编号,然后通过这个编号来找到底层系统中的操作以及方法,只不过在linux里面叫做文件描述符表,所以我们可以统一叫做这个来理解,还有就是为什么我们不能直接通过Java代码来操作一切,因为网络、文件、进程这些资源都属于操作系统的重要资源,不能让普通程序随便直接改,所以需要通过一个像文件描述符表的东西进行调度,调度到底层的操作系统的socket

socket的组成:

Socket(网络通信接口,大概念)
        │
        ├── TCP
        │    ├── Socket
        │    └── ServerSocket
        │
        └── UDP
             ├── DatagramSocket
             └── DatagramPacket

UDP

DatagramSocket = 通信工具
DatagramPacket = 要传输的数据包

DatagramSocket

构造方法作用常见场景
DatagramSocket()创建一个 UDP Socket,端口由系统自动分配客户端常用
DatagramSocket(int port)创建 UDP Socket,并绑定指定端口服务器常用
DatagramSocket(int port, InetAddress laddr)绑定指定端口和本机 IP一台机器有多个网卡/IP 时使用
方法作用
send(DatagramPacket p)发送 UDP 数据包
receive(DatagramPacket p)接收 UDP 数据,并保存到 p
close()关闭 Socket,释放网络资源
getLocalPort()获取当前 Socket 绑定的本地端口
getLocalAddress()获取当前 Socket 绑定的本地 IP
isClosed()判断 Socket 是否已经关闭

DatagramPacket

1,接收数据:

new DatagramPacket(byte[] buf, int length)

参数作用
buf用来存放收到的数据
length最多允许接收多少字节

2,发送数据:

new DatagramPacket(
    byte[] buf,
    int length,
    InetAddress address,
    int port
)

参数作用
buf要发送的数据
length要发送多少字节
address对方的 IP 地址
port对方的端口号


回显服务器例子

注意一

为什么服务器要指定端口,而客户端不需要,因为一台服务器是用来服务很多的客户端的,指定端口是让客户端知道并且只传入到那一个端口里面去,但是客户端有很多,就得系统随机,并且有些同学很容易将一个概念混淆,就是觉得客户端的端口和服务器的端口得一致才能对接上,实际是不是的:这里也会涉及前文讲到的五原组

我们只要明确了这五个就行,不需要一致

不是说需要五元组吗,那为什么服务器里面好像少了一个自己的ip:因为系统会根据客户端的数据进行匹配

比如客户端发送:
目标 IP = 192.168.1.10
目标端口 = 9090
协议 = UDP

到我们电脑之后,系统看到的信息就是

目的 IP:192.168.1.10
目的端口:9090
协议:UDP

假设你服务器中写到:

new DatagramSocket(9090);

也就相当于是:

监听:所有本机 IP 的 UDP 9090

然后进行匹配,最终就可以得出服务器自己的ip

所以说五元组往往是客户端和服务器两边的信息共同组成的!!!!!!!!

注意二

局域网 ≈ 一个小区
IP    ≈ 楼号 / 房屋地址
端口  ≈ 房间号

所以为什么会出现端口号冲突,也就是我们现在写的程序是在自己的电脑上面,所以ip都是127.0.0.1,所以如果服务器和客户端的端口号也一致的情况下,就会产生端口号冲突,也就是走到了一个小区的一个楼层中的一个房间,那我们电脑上有那么多的应用程序,为什么不会造成冲突呢,因为系统会给客户端的端口自动分配,所以我们程序里面的客户端的代码也没有指定端口号,简单介绍一下电脑里的ip

不同 IP 的来源不一样:

127.0.0.1:操作系统按照网络协议标准预留的,本机固定使用。

192.168.x.x 这种局域网 IP:一般是你家路由器通过 DHCP 自动分配给电脑。

公网 IP:通常由运营商分配给你的路由器/网络出口。

VPN 的 IP:由 VPN 软件或 VPN 服务器分配。

所以我们的应用程序的ip都是通过操作系统自己分配的,比如你点开来一个网站,系统会识别这个请求是从WiFi网卡发出去的,所以会分配192开头的那个

具体代码

package network;
//服务器

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

public class UdpEchoServer {
    //首先创建一个socket对象
    DatagramSocket serverSocket=null;
    //通过构造方法来完整
    //创建一个服务器对象,需要指定端口
    public UdpEchoServer(int port) throws Exception{
        serverSocket=new DatagramSocket(port);
    }
    //开始方法
    public void start() throws IOException {
        System.out.println("服务器启动了");
        while (true) {
            //先得创建一个datagrampacket对象,这个对象是传输数据的基本单位,就相当于你赶海一人拿一个桶是一样的
            DatagramPacket requestPacket = new DatagramPacket(new byte[1024], 1024);
            //接收请求
            serverSocket.receive(requestPacket);
            //将请求转为字符串
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //根据请求计算响应
            String response = process(request);
            //将响应封装到datagrampacket对象中,并且返回
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            //requestPacket.getSocketAddress()不仅拿到了ip也拿到了端口号
            serverSocket.send(responsePacket);
            //打印日志
            System.out.printf("客户端:%s 请求:%s 响应:%s\n", requestPacket.getSocketAddress(), request, response);
        }
    }

    private String process(String request) {
        //因为回显,所以就原路返回就行
        return request;
    }

    public static void main(String[] args) throws Exception {
        UdpEchoServer udpEchoServer=new UdpEchoServer(9090);
        udpEchoServer.start();
    }
}
package network;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Scanner;

//客户端
public class UdpEchoClient {
    //也是先创建一个socket对象
    DatagramSocket clientSocket=null;
    //需要给定服务器的ip以及端口
    private String serverIp;
    private int serverPort;
    public UdpEchoClient(String serverIp,int serverPort) throws SocketException {
        clientSocket=new DatagramSocket();
        this.serverIp=serverIp;
        this.serverPort=serverPort;
    }
    public void start() throws Exception{
        while(true){
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入要发送的内容:");
            String request=scanner.nextLine();
            if (request.equals("exit")){
                break;
            }
            DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
                    InetAddress.getByName(serverIp),serverPort);
            clientSocket.send(requestPacket);
            DatagramPacket responsePacket=new DatagramPacket(new byte[1024],1024);
            clientSocket.receive(responsePacket);
            //将服务器返回的数据打印出来
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            // 4. 显示响应.
            System.out.println(response);
        }
    }

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

注意三

别人要访问我的服务器:

本机测试用 127.0.0.1,想让互联网中的别人访问,通常需要“公网 IP + 开放端口”,云服务器就是最常见的实现方式。

127.0.0.1表示当前这台电脑自己。

查字典案例

我们刚刚通过一个回显的案例来初步了解了一下UDP的构造以及相关socket的api,那我们现在再用一个查字典的案例来进一步了解一下

注意一

服务器以及客户端的主要流程

服务器大体

在while循环里面

第一步:读取请求并解析

(因为我们这里是需要返回一个字符串,所以转为字符串格式)

第二步:根据请求计算相应

(这里以回显为例子,所以只需要原路返回就行)

第三步:把相应返回给客户端

客户端大体

第一步:在控制台读取输入

第二步:构造请求发送给服务器

第三步:接收服务器发来的响应

第四步:根据返回来的响应进行显示结果

所以我们可以总结一下:我们写服务器以及客户端,只需要跟着上面的步骤,在发送和接收的时候,来书写对的DatagramPacket对象,使用正确的构造方法

注意二

为什么可以这么写:因为其实现在市面的大部分背单词软件都是这么写的,我们也可以上网复制中英文对应关系来使我们的回答更加全面

具体代码

package network;

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

public class UdpDictServer extends UdpEchoServer{
    //因为我们是一个查字典,所以我们用到map来根据key来查找values
    Map<String,String> map=new HashMap<String, String>();
    //重写方法
    public UdpDictServer(int port) throws Exception {
        super(port);
        //在这里传入对应的中英文关系
        map.put("apple","苹果");
        map.put("banana","香蕉");
        map.put("cat","猫");
        map.put("dog","狗");
    }
    @Override
    public String process(String request) {
        //request 是客户端传来的查询内容,是 key;getOrDefault() 返回的是这个 key 对应的 value。
        return map.getOrDefault(request,"查无此词");
    }
    public static void main(String[] args) throws Exception {
        UdpDictServer server = new UdpDictServer(9090);
        server.start();
    }
}

(因为这里的客户端和前面的回显服务器一样,都是发送,包装成String,然后发送回去,所以我们还是用的之前的客户端)

TCP

ServerSocketSocket

客户端                                      服务器

Socket
  │
  │ 请求连接
  ├──────────────────────────→ ServerSocket
  │                                │
  │                             accept()
  │                                │
  │                                ↓
  │                             Socket
  │                                │
  └──────────── TCP连接 ────────────┘

整体的关系差不多就是这样,serversocket负责将将请求引进家门(处理客户端的连接,所以只会在服务器中写到),而socket像是专门负责业务的专员(在客户端以及服务器里都会用到)

回显服务器例子

TCP的短长连接

短连接:就是连接一次,通信完就断,需要多次进行连接和断开,会影响效率,但是不会一直占着服务器资源

长连接:连接一次,可以通信很多次之后关闭,就像我们程序中写到的while循环,不用老是创建和关闭,所以效率会高,但是会长时间占着资源,可能一直占着一个线程

注意一

accept建立连接:

因为在我们没有写多线程的时候,就只有一个socket服务员,也就是只能一个线程在进行服务,后面的就得等着,所以我们通过多线程的形式,可以实现多客户端

注意二

flush:

在我们用printwriter(这个内置了缓冲区)进行写入的时候,会先将数据写进缓冲区里,数据满了之后才会将数据传输出去,所以我们得写flush,写入一些数据就将他们传输过去

TCP和UDP的有无连接

TCP的Socket socket = serverSocket.accept()这个方法,会进行连接,也就是会拿到这个socket的信息并且记录下来,所以后续就会根据这个信息给特定的socket进行交互,但是udp不会记录对方socket信息

注意三

具体代码

package network;

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 {
    Socket socket=null;
    TcpEchoClient(String serverName,int serverPort) throws IOException {
        socket=new Socket(serverName,serverPort);
    }
    public void start() throws IOException {
        System.out.println("client start!");
        //依旧是先创建字节流的输入输出来处理数据
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            Scanner scanner = new Scanner(System.in);
            Scanner scannerNetwork = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                //先读取数据
                System.out.println("client:请输入数据");
                String request = scanner.next();
                //再将读取的数据传给服务器
                printWriter.println(request);
                printWriter.flush();
                //最后读取服务器的相应
                if (!scannerNetwork.hasNext()) {
                    break;
                }
                String response = scannerNetwork.next();
                //最后打印出来
                System.out.println("server: " + response);
            }

        }catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) throws IOException {
        TcpEchoClient client = new TcpEchoClient("127.0.0.1", 9090);
        client.start();
    }
}
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.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TcpEchoServer {
    //先创建一个ServerSocket对象
    ServerSocket serverSocket = null;

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

    }

    //开始方法
    public void start() throws IOException {
        //先建立连接,然后由Socket进行处理
        //注意一
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true) {
            Socket socket = serverSocket.accept();
            executorService.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(), socket.getPort());
        //tcp是面向字节流的
        try (InputStream inputStream = socket.getInputStream();
             OutputStream outputStream = socket.getOutputStream()) {
            //因为tcp是面向字节流的,所以我们定义了InputStream和OutputStream,但是我们输入是一个字符串,所以我们会
            //继续用到scanner来读和PrintWriter来写,并且PrintWriter可以将我们输入的字符串转为字节传输出去
            Scanner scanner = new Scanner(inputStream);
            PrintWriter printWriter = new PrintWriter(outputStream);
            while (true) {
                if (!scanner.hasNext()) {
                    // 客户端关闭了连接
                    System.out.printf("[%s:%d] 客户端下线!\n", socket.getInetAddress(), socket.getPort());
                    // 假设这里有一系列的逻辑呢??
                    break;
                }
                //先接收数据
                String request = scanner.next();
                //再根据请求计算相应
                String response = process(request);
                //最后发送相应给客户端
                printWriter.println(response);
                //注意二
                printWriter.flush();
                System.out.printf("[%s:%d] 客户端请求: %s\n", socket.getInetAddress(), socket.getPort(), request);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            socket.close();
        }
    }

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

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

查字典案例

package network;

import java.util.HashMap;
import java.util.Map;

public class TcpDictServer extends TcpEchoServer{
    Map<String,String> map=new HashMap<String, String>();

    public TcpDictServer(int port) throws Exception {
        super(port);
        map.put("apple","苹果");
        map.put("banana","香蕉");
        map.put("cat","猫");
        map.put("dog","狗");
    }
    @Override
    public String process(String request) {
        //request 是客户端传来的查询内容,是 key;getOrDefault() 返回的是这个 key 对应的 value。
        return map.getOrDefault(request,"查无此词");
    }
    public static void main(String[] args) throws Exception {
        TcpDictServer server = new TcpDictServer(9090);
        server.start();
    }
}

Logo

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

更多推荐