首页 > 其他分享 >网络协议基础(2):socket套接字及TCP、UDP的实现

网络协议基础(2):socket套接字及TCP、UDP的实现

时间:2024-10-25 16:20:36浏览次数:8  
标签:sockaddr UDP socket int TCP sockfd 接字 Socket addr

socket套接字及TCP、UDP的实现

socket套接字

Socket(套接字)是网络编程的核心概念,用于实现网络上不同设备或进程之间的双向数据通信。在不同的计算机之间进行通信时,Socket提供了一种标准化的接口,隐藏了底层的复杂协议,使网络编程更加方便。下面将从概念、类型、通信流程、编程接口以及实际应用等方面深入讲解Socket套接字。

socket 的基本概念

Socket(套接字):Socket是一种网络编程接口,通过它,程序可以在网络上接收或发送数据,通常由 IP 地址和端口号一起使用来标识特定的通信端点。

IP 地址:标识网络中唯一的设备位置。
端口号:标识网络中同一设备上运行的不同服务或应用。结合IP地址和端口号,Socket提供了唯一标识的通信端点。

每个socket可以看成一个通道,两端分别是通信双方,可以是客户端和服务器端。

socket的类型

Socket根据其支持的协议和特性不同,有多种类型,常见的有:

  1. 流式套接字(SOCK_STREAM):
    基于 TCP 协议:提供可靠的、面向连接的通信。
    特点:数据传输有序、可靠,传输过程中的数据包不会丢失。
    应用场景:适合需要高可靠性的应用,如文件传输、HTTP、数据库访问等。

  2. 数据报套接字(SOCK_DGRAM):
    基于UDP 协议:提供无连接的数据报通信。
    特点:数据传输无序、不保证可靠性(可能会丢失或乱序)。
    应用场景:适合对实时性要求高但对数据可靠性要求低的应用,如视频、音频流传输、DNS查询等。

  3. 原始套接字(SOCK_RAW):
    特点:允许直接访问底层协议(如IP协议),对网络报文进行完全控制。
    应用场景:网络调试、开发网络协议、数据包捕获(如Wireshark)等低层操作。

  4. 流控套接字(SOCK_SEQPACKET):
    提供有序和可靠的消息传输(结合了SOCK_STREAM的可靠性和SOCK_DGRAM的面向数据报特性),常用于实时通信。

Socket的工作流程

网络通信通常包括服务器和客户端,以下是Socket在建立连接和通信中的一般步骤,以常用的TCP连接为例:

服务器端工作流程

  1. 创建Socket:调用 socket() 函数创建一个套接字对象。
  2. 绑定 IP 和端口:使用 bind() 函数将套接字绑定到指定的IP地址和端口号,成为一个通信端口。
  3. 监听连接:调用 listen() 函数,使服务器端Socket进入监听状态,等待客户端的连接请求。
  4. 接受连接:使用 accept() 函数接收客户端的连接请求,成功建立连接后生成一个新的套接字,用于与客户端通信。
  5. 数据传输:服务器与客户端之间可以使用 send() 和 recv() 函数进行数据传输。
  6. 关闭Socket:使用 close() 函数关闭套接字,释放资源。

客户端工作流程

  1. 创建Socket:调用 socket() 函数创建一个套接字对象。
  2. 连接服务器:使用 connect() 函数向服务器发出连接请求。
  3. 数据传输:客户端和服务器端进行数据传输。
  4. 关闭Socket:使用 close() 函数关闭套接字,释放资源。
    在这里插入图片描述

Socket的编程接口(C++ 示例)

在 C/C++ 网络编程中,使用一组标准的 API 来管理和处理网络通信。通过这些 API,程序可以创建网络连接、发送和接收数据。主要的网络编程 API 包括 创建 Socket、绑定地址、监听和接收连接、数据传输和关闭连接 等。下面详细介绍这些常用的 API 函数及其作用。

1. 创建 Socket

int socket(int domain, int type, int protocol);	

作用:用于创建一个新的套接字(Socket)。

参数:
domain:地址族,指定使用的协议族,AF_INET对应IPv4,AF_INET6对应IPv6。
type:套接字类型,指定通信的特性,SOCK_STREAM:基于 TCP 协议的流式套接字,SOCK_DGRAM:基于 UDP 协议的数据报套接字。
protocol:协议类型,一般为 0(自动选择适合的协议)。
返回值:成功时返回套接字的文件描述符,失败时返回 -1。

示例:

int sockfd = socket(AF_INET, SOCK_STREAM, 0); //IPV4 TCP
if (sockfd == -1) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

2. 绑定地址

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

作用:将套接字绑定到特定的 IP 地址和端口号,使其可以接收发往该地址和端口的连接或数据包。

参数:

sockfd:套接字描述符(由 socket 返回)。
addr:指向 sockaddr 结构体的指针,包含服务器 IP 地址和端口号,。
addrlen:addr 的大小。
返回值:成功时返回 0,失败时返回 -1。

示例:

struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口
address.sin_port = htons(8080);       // 绑定到端口 8080

if (bind(sockfd, (struct sockaddr*)&address, sizeof(address)) < 0) {
    perror("bind failed");
    exit(EXIT_FAILURE);
}

3. 监听连接

int listen(int sockfd, int backlog)

作用:使套接字进入监听状态,准备接收客户端的连接请求。

参数:
sockfd:服务器的套接字描述符。
backlog:等待连接队列的最大长度。
返回值:成功时返回 0,失败时返回 -1。

示例:

if (listen(sockfd, 3) < 0) {
    perror("listen failed");
    exit(EXIT_FAILURE);
}

4. 接受连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

作用:从连接队列中取出一个客户端连接,返回新的套接字用于通信。

参数:
sockfd:服务器的监听套接字。
addr:指向 sockaddr 结构体的指针,存储客户端的 IP 地址和端口。
addrlen:addr 的大小。
返回值:成功时返回新的套接字描述符,失败时返回 -1。

示例:

int new_socket = accept(sockfd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
if (new_socket < 0) {
    perror("accept failed");
    exit(EXIT_FAILURE);
}

5. 连接到服务器

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

作用:客户端使用此函数向服务器发起连接请求。

参数:
sockfd:客户端的套接字描述符。
addr:服务器的 sockaddr 结构体,包含 IP 和端口。
addrlen:addr 的大小。
返回值:成功时返回 0,失败时返回 -1。

示例:

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);

inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
    perror("connect failed");
    exit(EXIT_FAILURE);
}

6. 发送数据

int send(int sockfd, const void *buffer, size_t length, int flags)

作用:用于向连接的另一端发送数据。

参数:
sockfd:套接字描述符。
buffer:要发送的数据缓冲区。
length:缓冲区的大小。
flags:传输标志,通常设为 0。
返回值:成功时返回发送的字节数,失败时返回 -1。

示例:

const char *message = "Hello from client";
send(sockfd, message, strlen(message), 0);

7. 接收数据

int recv(int sockfd, void *buffer, size_t length, int flags)

作用:从连接的另一端接收数据。

参数:
sockfd:套接字描述符。
buffer:接收数据的缓冲区。
length:缓冲区的大小。
flags:传输标志,通常设为 0。
返回值:成功时返回接收的字节数,失败时返回 -1。

示例:

char buffer[1024] = {0};
int bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);
printf("Received: %s\n", buffer);

8. 关闭 Socket

int close(int sockfd)

作用:关闭套接字,释放资源。

参数:
sockfd:要关闭的套接字描述符。
返回值:成功时返回 0,失败时返回 -1。

示例:

close(sockfd);

socket 相关的结构体

在网络编程中,Socket 结构体是存储网络连接相关信息的核心数据结构之一。C/C++ 提供了一系列的结构体用于管理 IP 地址、端口、协议等信息,尤其是在 sockaddr 和 sockaddr_in 等结构体中,这些结构体帮助应用程序配置 Socket 的各种参数,便于网络通信的建立和管理。

sockaddr 结构体

sockaddr 是一个通用的地址结构体,用于不同的协议族(如 IPv4、IPv6、Unix 域套接字等)统一存储地址信息。

struct sockaddr {
    sa_family_t sa_family; // 地址族,比如 AF_INET (IPv4) 或 AF_INET6 (IPv6)
    char sa_data[14];      // 地址数据,通常包含 IP 地址和端口信息
};

sa_family:指定地址族,例如 AF_INET 表示 IPv4,AF_INET6 表示 IPv6。
sa_data:存放 IP 地址和端口等信息,sa_data 并不直接存储 IPv4 或 IPv6 地址,而是提供一个通用的 14 字节数组,便于不同协议族的数据结构向其进行转换。

sockaddr_in 结构体(用于 IPv4)

IPv4 使用 sockaddr_in 结构体来管理网络地址。sockaddr_in 是 sockaddr 的一个特化结构,专门用于 IPv4,便于访问 IPv4 地址和端口。

struct sockaddr_in {
    sa_family_t sin_family; // 地址族,IPv4 用 AF_INET
    in_port_t sin_port;     // 端口号(需要使用网络字节序)
    struct in_addr sin_addr; // IP 地址
    char sin_zero[8];       // 填充字段,不使用
};
struct in_addr {
    uint32_t s_addr; // 存放 IP 地址(网络字节序)
};

sin_family:地址族,IPv4 使用 AF_INET。
sin_port:端口号,通常使用 htons() 转换为网络字节序。
sin_addr:IP 地址,通常是 in_addr 结构体。
sin_zero:填充字段,使得结构体大小与 sockaddr 保持一致。

在使用时,需要将 sockaddr_in 转换成通用的 sockaddr 类型,比如使用 bind() 或 connect() 函数时,需要强制转换。

sockaddr_in6 结构体(用于 IPv6)

sockaddr_in6 结构体用于存储 IPv6 地址信息。它是 sockaddr 的特化结构,用于支持更长的 IPv6 地址格式。

struct sockaddr_in6 {
    sa_family_t     sin6_family;   // 地址族,IPv6 用 AF_INET6
    in_port_t       sin6_port;     // 端口号(需要网络字节序)
    uint32_t        sin6_flowinfo; // 流信息(一般不常用)
    struct in6_addr sin6_addr;     // IPv6 地址
    uint32_t        sin6_scope_id; // 范围 ID(用于本地连接等)
};
struct in6_addr {
    unsigned char s6_addr[16]; // 存储 128 位 IPv6 地址
};

sin6_family:地址族,IPv6 使用 AF_INET6。
sin6_port:端口号,和 sockaddr_in 一样,通常使用 htons() 转换为网络字节序。
sin6_flowinfo:流信息字段,通常用于流控制,设置为 0 即可。
sin6_addr:IP 地址,通常是 in6_addr 结构体。
sin6_scope_id:范围 ID,用于指定区域范围,常用于本地连接。

IPv6 地址比 IPv4 更长,in6_addr 使用 16 字节数组 s6_addr 存储完整的 IPv6 地址。

使用 sockaddr 和 sockaddr_in/sockaddr_in6

当调用 bind()、connect() 等网络函数时,系统要求使用 sockaddr 结构体作为参数,而实际中一般用的是 sockaddr_in(IPv4)或 sockaddr_in6(IPv6)。所以需要将具体的结构体指针转换为通用的 sockaddr* 类型。

例如,使用 bind() 绑定一个 IPv4 地址:

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080); // 设置端口,使用网络字节序
    inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); // 设置 IP 地址
    bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    // 后续使用 listen()、accept() 等函数
}

其他常用 API

sendto 和 recvfrom:用于 UDP 数据报套接字的发送和接收,支持指定目标地址。

setsockopt 和 getsockopt:设置和获取套接字选项,例如设置超时时间、启用重用地址等。

select 和 poll:用于多路复用,可以同时监视多个套接字的事件(如读、写、异常)以实现非阻塞 I/O。

fcntl:可以设置套接字为非阻塞模式。

小结

这些 API 提供了 C/C++ 网络编程的基本操作,从创建和绑定套接字,到监听、接收连接,再到数据传输和关闭连接。这些函数组成了网络编程的核心接口,通过它们,开发者可以实现客户端-服务器、数据报通信、多路复用等复杂的网络通信模型。
以下是一个基于 TCP 的简单的客户端-服务器程序示例,展示了服务器和客户端的基本流程。

TCP、UDP的实现

服务器端代码示例(基于TCP)

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>

int main() {
    int server_fd, client_fd;
    struct sockaddr_in address;
    int addrlen = sizeof(address);

    // 创建套接字
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 设置IP和端口
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有本地地址
    address.sin_port = htons(8080);       // 设置端口号

    // 绑定套接字
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));

    // 监听连接
    listen(server_fd, 3);

    std::cout << "Server listening on port 8080..." << std::endl;

    // 接收客户端连接
    client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
    std::cout << "Client connected." << std::endl;

    // 发送数据
    const char *message = "Hello from server";
    send(client_fd, message, strlen(message), 0);

    // 关闭连接
    close(client_fd);
    close(server_fd);
    return 0;
}

客户端代码示例(基于TCP)

#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char buffer[1024] = {0};

    // 创建套接字
    sock = socket(AF_INET, SOCK_STREAM, 0);

    // 设置服务器地址和端口
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(8080);
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

    // 连接服务器
    connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    // 接收服务器消息
    read(sock, buffer, 1024);
    std::cout << "Message from server: " << buffer << std::endl;

    // 关闭连接
    close(sock);
    return 0;
}

服务端代码示例(基于UDP)

UDP是无连接的协议,这意味着不需要像TCP那样监听连接、接受连接或管理连接状态。

#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr, client_addr;
    char buffer[BUFFER_SIZE];
    
    // 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        return -1;
    }

    // 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    // 绑定地址和端口
    if (bind(sockfd, (const struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        close(sockfd);
        return -1;
    }

    std::cout << "Server listening on port " << PORT << std::endl;

    while (true) {
        socklen_t len = sizeof(client_addr);
        int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&client_addr, &len);
        buffer[n] = '\0'; // null-terminate the received string
        std::cout << "Received message: " << buffer << std::endl;

        // 回发接收到的消息
        sendto(sockfd, buffer, n, 0, (struct sockaddr*)&client_addr, len);
    }

    close(sockfd);
    return 0;
}

客户端代码示例(基于UDP)

#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024

int main() {
    int sockfd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE];

    // 创建 UDP 套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        return -1;
    }

    // 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    // 输入消息并发送到服务器
    std::cout << "Enter message: ";
    std::cin.getline(buffer, BUFFER_SIZE);

    sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));

    // 接收服务器的回应
    socklen_t len = sizeof(server_addr);
    int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&server_addr, &len);
    buffer[n] = '\0'; // null-terminate the received string

    std::cout << "Server response: " << buffer << std::endl;

    close(sockfd);
    return 0;
}

Socket编程中的常见问题和注意事项

  1. 资源管理:及时关闭Socket以释放系统资源,记得及时close不用的套接字。
  2. 错误处理:网络通信可能遇到各种错误,需检查每个操作的返回值。
  3. 多线程和异步编程:在高并发场景中,服务器通常使用多线程或异步编程来处理多个客户端请求。
  4. 缓冲区管理:数据传输过程中,合理的缓冲区设置可提高效率并避免数据丢失。

标签:sockaddr,UDP,socket,int,TCP,sockfd,接字,Socket,addr
From: https://blog.csdn.net/weixin_69480212/article/details/143234718

相关文章

  • C# UDP组播客户端【UDPClient】
    方式一UdpClientudp=newUdpClient(5566);//要通过其进行通信的本地端口号。5566是源端口udp.JoinMulticastGroup(IPAddress.Parse("224.0.0.4"));//将UdpClient添加到多播组;IPAddress.Parse将IP地址字符串转换为IPAddress实例IPEndPointmu......
  • C# UDP广播启动服务和客户端【Socket】
    服务端:Socketsocket=newSocket(AddressFamily.InterNetwork,SocketType.Dgram,ProtocolType.Udp);//初始化一个Scoket协议IPEndPointiep=newIPEndPoint(IPAddress.Any,9095);//初始化一个侦听局域网内部所有IP和指定端口EndPointe......
  • TCP连接状态是TIME_WAIT的场景解析
    在Tomcat处理网络请求时,TIME_WAIT状态通常是TCP连接关闭过程中的一个阶段。这个状态主要与TCP的四次挥手(Four-WayHandshake)有关。以下是在Tomcat处理网络请求时,连接状态变为TIME_WAIT的具体情况:四次挥手过程1.客户端发送FIN包:客户端完成数据传输后,主动调用clos......
  • 【ModbusTCP与Profibus DP双向互转说明】
        Profibusdp和ModbusTCP均为工业通信协议。ModbusTCP为串行通讯协议,已成为工业领域通讯协议的业界标准。Modbus是现在国内工业领域应用最多的协议,不只PLC设备,各种终端设备,比如水控机、水表、电表、工业秤、各种采集设备。而Profibus为自动化技术的现场总线标准,广泛......
  • TCP连接的状态
    TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP连接的状态可以通过一个状态机来描述,这个状态机定义了TCP连接从建立到关闭过程中可能经历的各种状态。一、状态状态名称描述触发条件CLOSED表示没有连接。这是初始状态。无LISTEN服务......
  • 【接口】websocket
    背景HTTP(超文本传输协议)缺点:缺乏数据加密、身份验证和会话管理等安全特性;HTTP链接的半双工的,而且通信只能由客户端发起,服务端无法将数据主动推送给客户端HTTPS(安全超文本传输协议):为了解决HTTP的缺点,提出HTTPS,提供传输的安全性websocket:为了解决"HTTP链接的半双工的,而且通信只......
  • 鸿蒙网络编程系列35-通过数据包结束标志解决TCP粘包问题
    1.TCP数据传输粘包简介在本系列的第6篇文章《鸿蒙网络编程系列6-TCP数据粘包表现及原因分析》中,我们演示了TCP数据粘包的表现,如图所示:随后解释了粘包背后的可能原因,并给出了解决TCP传输粘包问题的两种思路,其中一种就是指定数据包结束标志,本节将通过一个示例演示这种思路......
  • 【保姆级IDF】ESP32使用WIFI作为AP模式TCP通信:连接客户端+一对多通信
    #1024程序员节|征文#Tips:抛砖引玉,本文记录ESP32学习过程中遇到的收获。如有不对的地方,欢迎指正。1.前言    关于ESP32的WIFI这部分基础知识,在网上可以找到许多,包括TCP协议、套接字等等,博主之前的文章也有介绍,在此本文不再赘述,直接讲清楚标题功能如何实现,并说明......
  • [Go] 如何妥善处理 TCP 代理中连接的关闭
    如何妥善处理TCP代理中连接的关闭相比较于直接关闭TCP连接,只关闭TCP连接读写使用单工连接的场景较少,但通用的TCP代理也需要考虑这部分场景。背景今天在看老代码的时候,发现一个TCP代理的核心函数实现的比较粗糙,收到EOF后直接粗暴关闭两条TCP连接。funcConnCat(u......
  • C++Socket通讯样例(服务端)
    1.创建Socket实例并开启。privateintOpenTcp(intport,stringip=""){//1.开启服务端try{_tcpServer=newSocket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);IPAddressipAddr=IPAddress.Any;......