JAVA网络编程——socket套接字的介绍上(详细)

#新星杯·14天创作挑战营·第11期#

目录

前言

常见的网络通信的基础概念 (前置知识)

1.网络编程

2. IP 地址 和 端口号

3. 协议 

什么是网络协议?

常见协议有哪些?

协议的分层设计

 4.五元组

Socket套接字

 什么是socket

为什么需要 socket?

 socket 的类型(流式 / 数据报)

一、流式套接字(Stream Socket)

二、数据报套接字(Datagram Socket)

UDP数据报套接字编程 

DatagramSocket(UDP 套接字)

 DatagramPacket(数据报)

 UDP回显式服务器

UDP客户端

客户端为什么通常使用随机端口

 原因 1:客户端不提供服务,只发起请求

原因 2:同一台机器可以开启多个客户端实例

 完整代码

结尾


前言

大家好! 今天笔者分享一篇关于 Socket 套接字 的博客,希望能为正在学习网络编程的读者提供一些帮助。

本篇博客的目的如下:

  1. 梳理网络通信的基础概念
    本文还将顺带总结一些与 Socket 密切相关的基础知识,包括:IP 地址、端口号、网络协议、五元组、协议分层模型的作用与内容、TCP/UDP 区别、Socket API 的使用方式,以及如何通过代码模拟客户端/服务器之间的通信过程。

  2. 理解客户端与服务器的数据交互
    学习 Socket 的过程,也是在理解“客户端和服务器之间是如何发送和接收数据的”、“为什么访问网页可以看到内容”的过程。我们将从原理上搞清楚这些关键问题。

  3. 打下网络编程基础
    Socket 套接字是很多网络通信方式的基础,例如:HTTP 请求、WebSocket、RPC 调用、消息队列等,底层几乎都离不开 Socket 的支持。

虽然上面这些内容看起来有些杂,但每一部分都是网络编程的重要知识。
笔者每一篇博客都尽量用心打磨(通常花费不下 180 分钟),并力求逻辑清晰、内容准确。
希望笔者能耐心阅读,如果能从中有所收获,那将是我最大的动力。那么,接下来让我们开始吧

常见的网络通信的基础概念 (前置知识)

本部分内容主要是笔者从书籍、课程以及实际学习过程中总结出的一些网络通信的基础概念。它们构成了理解 Socket 的前提知识。

如果你已经对这些内容非常熟悉,可以直接跳过本节,进入 Socket 的核心部分。但如果读者对“IP地址”、“端口号”、“协议”这些词还有点模糊,不妨先看看下面的总结,打好网络编程的地基。

1.网络编程

网络编程(Network Programming)指的是通过编程的方式,让不同主机之间通过网络进行通信。通俗来说,它就是教计算机怎么“联网聊天”。

站在程序员的角度来看,网络编程就是编写代码,让进程之间能够通过网络收发数据,实时通信

2. IP 地址 和 端口号

IP地址(Internet Protocol Address)是指互联网协议地址,又译为网际协议地址。

在网络通信中,IP 地址就像一台计算机在网络中的“身份证号码”,用于标识一台设备。

IP 地址有两种形式:

  • IPv4(如:192.168.1.1):主流地址格式,4 个字节,表示 0~255 范围的四组数字。

  • IPv6:解决 IPv4 地址枯竭问题,长度更长.

那么,什么是端口呢?每一台主机通常会运行多个进程,这些进程可能都需要进行网络通信。此时,仅靠 IP 地址无法区分具体是哪个进程在通信。为了让网络数据准确地送达目标进程,操作系统为每个进程分配了不同的端口号,这就好比在一栋楼(主机)中给不同的住户(进程)分配了不同的门牌号。

在网络通信中,IP 地址用于标识主机,而端口号则用于标识主机中的具体进程。
简单来说:IP 地址定位设备,端口号定位进程。
就像寄快递,不仅要知道收货地址(IP 地址),还需要指定收货人(端口号),否则就无法准确投递。

3. 协议 

在现实生活中,我们人与人之间交流要遵循语言规则,例如语法、词义、语气等,这样才能听得懂对方在说什么。同样,计算机之间的传输媒介是光信号和电信号。通过 "频率" 和 "强弱" 来表⽰ 0 和 1 这样的信息。要想传递 各种不同的信息,就需要约定好双⽅的数据格式。

什么是网络协议?

网络协议是计算机之间通信时必须遵循的规则和约定,它规定了数据如何格式化、如何传输、如何被接收和处理。只有发送方和接收方都遵守同一个协议,通信才能顺利进行。

常见协议有哪些?
  • HTTP / HTTPS(超文本传输协议):我们平时访问网页,用的就是 HTTP 协议,HTTPS 是它的加密版。

  • FTP(文件传输协议):用于在网络上传输文件。

  • SMTP / POP3 / IMAP:用于电子邮件的发送和接收。

  • TCP / UDP:底层传输协议,控制数据如何在网络中传递。

  • IP:负责寻址和路由,用于确保数据能从一台设备送达另一台设备。

  • DNS:域名系统协议,负责把我们熟悉的网址(例如 www.baidu.com)转换为对应的 IP 地址。

除此以外,还有一类自定义协议,一般出现在应用层,它们通常出现在以下几种情况:

  • 为了安全:比如防止别人抓包,就会把真实数据进行加密、编码,只有服务端知道解密方式。

  • 为了性能:标准协议像 HTTP 虽然通用,但体积较大,部分系统会使用更轻量的“自定义协议”传输数据,节省带宽、提高速度。

  • 为了适配业务需求:某些业务场景有特定的交互流程,标准协议不满足需求,就需要自定义规则来通信。

例如:

客户端发送的数据格式可能是:
"length=5|type=LOGIN|data=zhangsan"

服务端根据这些字段解析请求,并返回:
"code=200|msg=success"

这种格式看起来不像 HTTP、也不是 TCP/UDP,但它依然是协议,只不过是应用层定义的“私有协议”,开发者之间约定好格式和含义即可。 

协议的分层设计

 我们都知道互联网协议是分层的,每一层只关心自己该做的事情。例如:

  • 应用层(HTTP、FTP、SMTP...):提供服务给用户应用程序,处理业务逻辑。

  • 传输层(TCP、UDP):建立端到端的通信连接,确保数据按顺序送达或快速传输。

  • 网络层(IP、ICMP):负责路径选择和地址定位,把数据包从源头送到目标。

  • 数据链路层物理层:处理物理传输(电信号、网卡通信等)。

而我们的套接字是应用层与传输层之间的中间软件抽象层。

 4.五元组

在进行一次 TCP 或 UDP 通信时,可以用五元组来唯一标识一条连接: 

1.  源IP:标识源主机
2. 源端⼝号:标识源主机中该次通信发送数据的进程
3. ⽬的IP:标识⽬的主机
4. ⽬的端⼝号:标识⽬的主机中该次通信接收数据的进程
5. 协议号:标识发送进程和接收进程双⽅约定的数据格式

例如:

源 IP: 192.168.1.5  
源端口: 54321  
目标 IP: 172.217.160.78  
目标端口: 80  
协议: TCP

很显然, 五元组的定义与我们的socket套接字密切相关

Socket套接字

有了上面的前置知识铺垫,接下来我们正式介绍Socket套接字

 什么是socket

Socket套接字,是由系统提供⽤于⽹络通信的技术,是基于TCP/IP协议的⽹络通信的基本操作单元。 基于Socket套接字的⽹络程序开发就是⽹络编程。
简单来说, Socket 就像“程序与网络之间的通信接口”,程序可以通过它发送和接收网络数据。它屏蔽了底层复杂的网络细节,让开发者能够更容易地实现网络通信功能。

举个例子,如果你把 IP 地址比作一个房子的地址,把端口号比作房间号,那 Socket 就是门上那个“能收发快递的信箱”。程序通过这个信箱收快递(接收数据)、发快递(发送数据)。

为什么需要 socket?

当两个程序需要通过网络通信时,它们需要做几件事:

  • 先建立一条“通路”

  • 然后发送数据

  • 接着接收对方回应

而 Socket 就是帮助程序完成这一切的关键“桥梁”。它屏蔽了底层协议的复杂细节,让程序员可以更方便地通过代码建立网络连接,进行数据传输。

 socket 的类型(流式 / 数据报)

在网络编程中,Socket 可以分为两种常见类型(但只有这两种),分别对应两种主流的传输协议:TCP(Transmission Control Protocol)UDP(User Datagram Protocol)。我们通常将它们称为:

  • 流式套接字(Stream Socket) —— 对应 TCP 协议

  • 数据报套接字(Datagram Socket) —— 对应 UDP 协议

一、流式套接字(Stream Socket)

  • 基于 TCP 协议

  • 是一种 面向连接(connection-oriented) 的通信方式

  • 通信前,客户端和服务器之间必须先建立连接(三次握手)

  • 提供可靠的数据传输,数据不会丢失、不会乱序,适合需要稳定连接的场景

 特点:

  • 保证数据的可靠性和顺序性

  • 数据像“水流”一样连续传输,无明显边界

  • 适合:网页浏览、文件传输、聊天应用等对可靠性要求高的场景

示例:
我们使用浏览器访问一个网站,底层就是通过 TCP(流式套接字)建立连接,把请求发给服务器,再接收返回的网页内容。 

二、数据报套接字(Datagram Socket)

  • 基于 UDP 协议

  • 是一种 无连接(connectionless) 的通信方式

  • 不需要事先建立连接,谁想发就直接发(即发即走)

  • 数据不保证到达,也不保证顺序,但效率高、开销小

 特点:

  • 快速、效率高

  • 数据以“数据包”的形式独立发送,有边界

  • 不保证可靠性,适合对实时性要求高但能容忍部分数据丢失的场景

示例:
视频通话、实时语音、在线游戏中使用的就是 UDP(数据报套接字),即使中途丢了一帧,也不会重传,保证实时性更重要。

 对于java来说,由于两种套接字的通信模型截然不同,故而需要不同的API去调用.

UDP数据报套接字编程 

在JAVA中,使用UDP协议进行网络通信,主要依赖两个类:

 DatagramSocket :表示一个 UDP 套接字,用于发送和接收数据报。

 DatagramPacket:表示一个数据报报,封装了要发送或接收到的数据、长度、目标 IP 和端口等信息。

这两个类,前者用来发送和接收数据报,后者用来"制造"数据报

DatagramSocket(UDP 套接字)

构造方法:

// 用于发送端(客户端)
DatagramSocket socket = new DatagramSocket();

// 用于接收端(服务器端)
DatagramSocket socket = new DatagramSocket(int port);

 常用方法

方法名作用示例 / 说明
send(DatagramPacket p)发送数据报包将数据发送给远程主机
receive(DatagramPacket p)接收数据报包(阻塞)接收来自远程主机的数据
close()关闭套接字释放资源,防止端口被占用
setSoTimeout(int timeout)设置接收超时时间(毫秒)超时未接收到抛出 SocketTimeoutException
getLocalPort()获取当前绑定的端口号用于调试或日志
DatagramSocket socket = new DatagramSocket(8888);
socket.setSoTimeout(3000); // 设置3秒接收超时

 DatagramPacket(数据报)

 【发送数据时】的构造方法

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

参数说明

  • buf:要发送的数据内容(字节数组)。

  • length:实际发送的数据长度。

  • address:目标主机的 IP 地址。

  • port:目标主机的端口号。

 示例:

byte[] data = "Hello UDP!".getBytes();
InetAddress ip = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(data, data.length, ip, 8888);

 【接收数据时】的构造方法

DatagramPacket(byte[] buf, int length)

 参数说明

  • buf:用于存储接收到的数据的字节数组。

  • length:字节数组的长度(最大接收长度)。

 示例:

byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet);

 常用方法

方法名作用示例 / 说明
getData()获取数据内容(字节数组)接收端用来读取消息内容
getLength()获取实际接收到的字节长度getData() 一起用
getAddress()获取发送者的 IP 地址接收端获取客户端来源
getPort()获取发送者的端口号可用于响应客户端
setData(byte[] buf)设置要发送的数据可用于复用一个数据报对象
setLength(int length)设置有效数据长度适配不同的数据量

光讲理论也无济于事,接下来,笔者分享一组代码并给出仔细的说明,帮助大家理解怎么调用这些API

笔者的代码示例展示的是一个回显式服务器和一个客户端

回显式服务器: 客户端请求什么内容,就原封不动返回的服务器

 UDP回显式服务器

 首先我们把类构造出来,让我们的套接字指定一个端口监听是否有客户端发送请求

public class UdpEchoServer {
   public DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        // 指定了一个固定端口号, 让服务器来使用.  == 指定端口监听
        socket = new DatagramSocket(port);
    }
}

 接下来我们启动服务器,实现那么3个功能:

1.读取请求并且解析

2.根据请求,计算响应,此处是原封不动的返回

3.把响应发回给客户端

笔者先给出完整代码,然后每一步具体分析

    public void start() throws IOException {
        // 启动服务器
        System.out.println("服务器启动");

        while (true) {
            // 循环一次, 就相当于处理一次请求.
            // 处理请求的过程, 典型的服务器都是分成三个步骤的.
            // 1. 读取请求并解析.
            //    DatagramPacket 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            //    把读取到的二进制数据, 转成字符串. 只是构造有效的部分.
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            // 2. 根据请求, 计算响应. (服务器最关键的逻辑)
            //    但是此处写的是回显服务器. 这个环节相当于省略了.
            String response = process(request);

            // 3. 把响应返回给客户端
            //    根据 response 构造 DatagramPacket, 发送给客户端.
            //    此处不能使用 response.length()
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            //    此处还不能直接发送. UDP 协议自身没有保存对方的信息(不知道发给谁)
            //    需要指定 目的 ip 和 目的端口. requestPacket.getSocketAddress()
            socket.send(responsePacket);

            // 4. 打印一个日志
            System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
                    request, response);
        }
    }

 首先是第一步,我们需要先构造一个空的数据报,来存储收到的请求

      // 1. 读取请求并解析.
//DatagramPacket 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.

 DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
 socket.receive(requestPacket);
//    把读取到的二进制数据, 转成字符串. 只是构造有效的部分.
 String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

第二步 根据请求,计算响应

// 2. 根据请求, 计算响应. (服务器最关键的逻辑)
 //    但是此处写的是回显服务器. 这个环节相当于省略了.
String response = process(request);

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

第三步,把响应数据发回给客户端,这里我们要和五元组严格对应上,指出 目的IP和端口 ,因为UDP的特性造成了服务器并不知道客户端的IP和端口号

请注意,数据报的需要的是 byte 数组和 数组的长度,所以笔者写的是response.getBytes(), response.getBytes().length

DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,requestPacket.getSocketAddress());
            //    此处还不能直接发送. UDP 协议自身没有保存对方的信息(不知道发给谁)
            //    需要指定 目的 ip 和 目的端口. requestPacket.getSocketAddress()
            socket.send(responsePacket);

总体来说,逻辑是不难的,难的是我们需要仔细阅读帮助文档,明白数据报构造方法中每个参数的意义. 

UDP客户端

接下来我们写一个模拟的客户端,它的功能如下:

1.构造请求,并发送给服务器

2.读取服务器发送回来的请求

首先是构造套接字,和服务器的被动等待不同, 客户端的职责是主动发起通信,因此在构造数据包时,必须提供服务器的 IP 地址和端口号。

public class UdpEchoClient
{
    private DatagramSocket socket = null;

    // UDP 本身不保存对端的信息,自己的代码中保存一下
    private String serverIp;
    private int serverPort;

    // 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket(); // 站在使用者的角度来看,客户端一定要随机端口号
    }
}

 在这里为什么没有指定端口,而是随机端口呢?

首先,源 IP 是操作系统根据路由自动判断的,而这里的源端口也是随机的,为什么不像客户端一样指定呢?

原因大致如下:

什么是端口号(再强调一次)

在网络通信中,端口号是为了标识同一台主机中不同的进程。也就是说:

  • IP 地址确定哪台主机;

  • 端口号确定主机上的哪个程序(Socket);

客户端为什么通常使用随机端口
 原因 1:客户端不提供服务,只发起请求

客户端只负责发起请求、等待响应,不需要其他人来主动联系它。

所以客户端没有必要绑定固定端口,只要临时生成一个端口、能收发数据就行。

原因 2:同一台机器可以开启多个客户端实例

如果你写死一个端口(例如  9111),那你只能同时启动一个客户端程序,即,客户端只能通过你指定的端口号发送请求,如果有多个一样的客户端,就会产生冲突

 接下来是具体的功能实现了,由于讲解客户端的时候说的很细了,这里就简单带过

     public  void  start() throws IOException {

         Scanner scanner = new Scanner(System.in);
         while(true)
         {
             System.out.println("输入的内容:");
             if(!scanner.hasNext())
             {
                 break;
             }
             String request = scanner.nextLine();
             DatagramPacket requestPacket = new DatagramPacket(
                     request.getBytes(),
                     request.getBytes().length,
                     InetAddress.getByName(serverIp),
                     serverPort);  // 把数据包打包好

             socket.send(requestPacket); // 发送

             DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096); //  用来接收响应数据的数据包


             socket.receive(responsePacket);

             System.out.println("响应结果");

             String respon = new String(responsePacket.getData(),0,responsePacket.getLength());

             System.out.println(respon);

         }
     }

 输入内容, 打包好数据报,发送给服务器,接收并输出服务器返回的内容

 完整代码

笔者把完整代码贴出来,方便读者们自己实验

服务器:

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

// 回显式服务器
public class UdpEchoServer {
   public 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 表示一个 UDP 数据报. 此处传入的字节数组, 就保存 UDP 的载荷部分.
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(requestPacket);
            //    把读取到的二进制数据, 转成字符串. 只是构造有效的部分.
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());

            // 2. 根据请求, 计算响应. (服务器最关键的逻辑)
            //    但是此处写的是回显服务器. 这个环节相当于省略了.
            String response = process(request);

            // 3. 把响应返回给客户端
            //    根据 response 构造 DatagramPacket, 发送给客户端.
            //    此处不能使用 response.length()
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), response.getBytes().length,
                    requestPacket.getSocketAddress());
            //    此处还不能直接发送. UDP 协议自身没有保存对方的信息(不知道发给谁)
            //    需要指定 目的 ip 和 目的端口. requestPacket.getSocketAddress()
            socket.send(responsePacket);

            // 4. 打印一个日志
            System.out.printf("[%s:%d] req: %s, resp: %s\n", requestPacket.getAddress().toString(), requestPacket.getPort(),
                    request, response);
        }
    }

    // 后续如果要写别的服务器, 只修改这个地方就好了.
    // 不要忘记, private 方法不能被重写. 需要改成 public
    public String process(String request) {
        return request;
    }

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

客户端:

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

public class UdpEchoClient
{
    private DatagramSocket socket = null;

    // UDP 本身不保存对端的信息,自己的代码中保存一下
    private String serverIp;
    private int serverPort;

    // 和服务器不同, 此处的构造方法是要指定访问的服务器的地址.
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket(); // 站在使用者的角度来看,客户端一定要随机端口号
    }

     public  void  start() throws IOException {

         Scanner scanner = new Scanner(System.in);
         while(true)
         {
             System.out.println("输入的内容:");
             if(!scanner.hasNext())
             {
                 break;
             }
             String request = scanner.nextLine();
             DatagramPacket requestPacket = new DatagramPacket(
                     request.getBytes(),
                     request.getBytes().length,
                     InetAddress.getByName(serverIp),
                     serverPort);  // 把数据包打包好

             socket.send(requestPacket); // 发送

             DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096); //  用来接收响应数据的数据包


             socket.receive(responsePacket);

             System.out.println("响应结果");

             String respon = new String(responsePacket.getData(),0,responsePacket.getLength());

             System.out.println(respon);

         }
     }
    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1",9090);
        // 指定本机 IP , 和 指定好的端口号
        udpEchoClient.start();
    }
}

最后的效果截图如下:

 

结尾
 

本来笔者不想在这里结束的,因为还有很多内容没讲完,但是笔者此时的文本量达到了五位数,也已经连续写了3个小时了,为了保证质量,剩下的诸如TCP套接字如何使用,已经如何引入多线程就留在下篇说,感谢读者们的支持

评论 60
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值