socket通讯原理及例程(详解)

news2024/11/16 8:23:16

里面有疑问或者不正确的地方可以给我留言。

对TCP/IP、UDP、Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵。那么我想问:

  1. 什么是TCP/IP、UDP?
  2. Socket在哪里呢?
  3. Socket是什么呢?
  4. 你会使用它们吗?

什么是TCP/IP、UDP?

TCP/IP(传输控制协议/互联网协议,Transmission Control Protocol/Internet Protocol)是用于网络通信的核心协议,构成了互联网的基础。它定义了计算机和网络设备如何通过网络传输数据,并提供了可靠的端到端数据传输机制。

从上述这段话可以看出,TCP/IP是一个集合。什么集合?协议的集合目的就是是实现 网络互联和数据通信,使不同设备能够通过网络可靠且有效地交换信息。它包括以下协议:

  • Ethernet:局域网中使用的常见协议,定义了数据帧在有线网络上的传输方式。
  • Wi-Fi (Wireless Fidelity):无线局域网协议,基于IEEE 802.11标准。
  • PPP (Point-to-Point Protocol):用于在两个直接相连的节点之间传输数据,如电话线上。
  • SLIP (Serial Line Internet Protocol):早期用于通过串行连接传输IP数据包的协议。
  • IP (Internet Protocol):负责路由和寻址,将数据包从源主机发送到目标主机。包括:
    • IPv4:最广泛使用的IP协议,使用32位地址。
    • IPv6:升级版IP协议,使用128位地址,提供更多的IP地址。
  • ICMP (Internet Control Message Protocol):用于发送错误报告和网络诊断信息(如ping操作)。
  • ARP (Address Resolution Protocol):用于将IP地址转换为网络硬件地址(如MAC地址)。
  • RARP (Reverse ARP):将硬件地址转换为IP地址(较少使用)。
  • TCP (Transmission Control Protocol):提供可靠的、面向连接的数据传输协议,确保数据按顺序无错地到达。
  • UDP (User Datagram Protocol):提供不可靠、无连接的数据传输协议,适用于对速度要求较高且能容忍少量数据丢失的应用,如视频流和在线游戏。
  • HTTP (Hypertext Transfer Protocol):用于网页浏览,传输超文本。
  • HTTPS (HTTP Secure):HTTP的加密版本,通过SSL/TLS保护数据 传输。
  • FTP (File Transfer Protocol):用于在计算机之间传输文件。
  • SMTP (Simple Mail Transfer Protocol):用于发送电子邮件。
  • POP3 (Post Office Protocol 3):用于从邮件服务器下载电子邮件。
  • IMAP (Internet Message Access Protocol):用于从服务器获取电子邮件,支持在服务器上管理邮件。
  • DNS (Domain Name System):用于将域名转换为IP地址。
  • Telnet:提供远程登录服务。
  • SSH (Secure Shell):加密的远程登录协议,替代Telnet。

看到这里,是不是瞬间头大了,这什么东西?这么多!这么复杂!当时的人也觉得这一堆东西往这一堆,很磕碜,很不讲究,太TM影响心情了。

所以,就引出了教科书上经典的那句话,TCP/IP协议集是一个分层、多协议的通信体系。

一群聪明蛋按照协议的功能分工和网络通信过程中的逻辑顺序把协议划分为四层。这种分层设计使得网络协议更加灵活、可扩展,并能够解决不同通信问题。如下所示:

数据链路层(功能:处理硬件接口与底层网络通信,负责数据帧的发送与接收。

  • ARP (Address Resolution Protocol):用于将IP地址解析为物理网络地址(如MAC地址),适用于局域网。
  • RARP (Reverse ARP):将物理地址(如MAC地址)映射为IP地址,较少使用,已被DHCP替代。
  • Ethernet:局域网协议,定义了数据帧在有线网络中的传输方式。
  • Wi-Fi (Wireless Fidelity):无线局域网协议,基于IEEE 802.11标准。
  • PPP (Point-to-Point Protocol):用于在两点间通过串行链路传输数据。
  • SLIP (Serial Line Internet Protocol):早期的串行数据传输协议,已被PPP取代。

网络层(功能:负责路由与寻址,确保数据包能够从源地址到达目的地址。

  • IP (Internet Protocol):负责路由和寻址,将数据包从源主机发送到目标主机。包括:
    • IPv4:使用32位地址的IP协议,常用。
    • IPv6:使用128位地址的升级版IP协议,解决了IPv4地址耗尽问题。
  • ICMP (Internet Control Message Protocol):用于发送错误报告和网络诊断信息(如ping操作)。
  • IGMP (Internet Group Management Protocol):用于管理多播组的成员,支持多播通信。
  • NAT (Network Address Translation):允许多个设备使用一个公有IP地址,进行IP地址转换。
  • OSPF (Open Shortest Path First):一种用于路由选择的内部网关协议,基于链路状态的路由。
  • BGP (Border Gateway Protocol):用于不同自治系统之间的路由选择。

传输层(功能:负责提供端到端的通信服务,包括数据的分段、传输、错误检测与修复等。

  • TCP (Transmission Control Protocol):提供可靠、面向连接的数据传输,确保数据按顺序无差错地到达。
  • UDP (User Datagram Protocol):提供不可靠、无连接的数据传输,适用于对速度要求高、能容忍少量数据丢失的应用(如视频流、在线游戏)。

应用层(功能:提供应用程序使用的网络服务。

  • HTTP (Hypertext Transfer Protocol):用于网页浏览,传输超文本。
  • HTTPS (HTTP Secure):HTTP的加密版本,通过SSL/TLS保护数据传输。
  • FTP (File Transfer Protocol):用于在计算机之间传输文件。
  • SMTP (Simple Mail Transfer Protocol):用于发送电子邮件。
  • POP3 (Post Office Protocol 3):用于从邮件服务器下载电子邮件。
  • IMAP (Internet Message Access Protocol):用于从服务器获取电子邮件,支持在服务器上管理邮件。
  • DNS (Domain Name System):用于将域名转换为IP地址。
  • Telnet:提供远程登录服务,但不安全,因为数据是明文传输。
  • SSH (Secure Shell):安全的远程登录协议,替代Telnet,支持加密通信。

看着是不是还是有些头大,其实主要结构如下:

举例说明分层:

  • 当你使用浏览器访问一个网站时,浏览器使用应用层的HTTP协议来发送请求,这个请求会通过传输层的TCP协议分段并加上校验信息,再通过互联网层的IP协议选择路径传输到目标服务器,最后通过数据链路层的以太网协议发送数据帧到网络中。各层互不干扰,但又紧密配合完成整个通信过程。

那这些和Socket有什么关系呢?来个图就一目了然了,如下图所示:

Socket是应用层和传输层之间的接口,它将应用层的协议请求映射到传输层上的具体传输服务。Socket充当了网络通信的桥梁,既不属于应用层,也不属于传输层,而是应用程序用于访问传输层服务的一种API。

所以,Socket(套接字)是计算机网络编程中的一种通信机制,允许两个程序在不同的设备上进行数据交换。它提供了在不同主机之间通过网络传输数据的接口,是实现网络通信的重要工具。

常见的Socket类型:

(1)TCP Socket(面向连接,可靠的传输):TCP是一种面向连接的协议,确保数据包按顺序到达且不丢失。TCP Socket用于建立可靠的、持久的连接,如网页浏览器与服务器之间的通信。

(2)UDP Socket(无连接,非可靠的传输):UDP是一种无连接的协议,传输速度快,但不保证数据的可靠传输。UDP Socket常用于对实时性要求高但允许丢包的应用,如视频流、在线游戏等。

Socket 的工作原理基于“客户端-服务器”模型:

  1. 服务器端

    • 服务器程序在特定的IP地址和端口上“监听”(等待连接请求)。
    • 当客户端请求连接时,服务器会接受连接,双方通过Socket进行数据传输。
  2. 客户端

    • 客户端程序向服务器发起连接请求,通过服务器的IP地址和端口号找到目标服务器。
    • 连接建立后,客户端与服务器可以相互发送和接收数据。

Socket编程的基本步骤:

服务器端

1.创建套接字:socket()

在C++中,socket() 函数用于创建套接字(socket),它是网络编程的基础。通过创建套接字,应用程序可以在网络上进行通信。

socket()函数的语法

#include <sys/types.h>
#include <sys/socket.h>

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

参数说明

(1).domain(协议族/地址族): 指定使用的通信域,决定了套接字通信的地址格式。

常见的选项有:

  • AF_INET:IPv4协议的地址族。
  • AF_INET6:IPv6协议的地址族。
  • AF_UNIX:本地通信(也称为域套接字,主要用于同一台计算机上的进程通信)。

(2).type(套接字类型): 指定通信类型,决定了套接字的特性。

常见的选项有:

  • SOCK_STREAM:提供面向连接的可靠数据传输(TCP协议)。
  • SOCK_DGRAM:提供无连接的数据报传输(UDP协议)。
  • SOCK_RAW:提供对底层协议的直接访问,通常用于高级网络编程和自定义协议。

(3).protocol(协议): 通常指定为 0,表示使用默认协议。如果有多个协议可供选择,可以明确指定协议编号。例如:

  • IPPROTO_TCP:TCP协议。
  • IPPROTO_UDP:UDP协议。

返回值

  • 成功:返回一个文件描述符,表示新创建的套接字。
  • 失败:返回 -1,并设置 errno 以指示错误原因。

示例代码:

//
// Created by armstrong on 2024/9/9.
//
#include <sys/types.h>

#include <sys/socket.h>

#include <iostream>

#include <unistd.h>

#include <cstring>

#include <cerrno>

int main(){
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    if(sockfd == -1){
        std::cerr << "Failed to create socket. Error: " << strerror(errno) << std::endl;
        return -1;
    }
    std::cout << "Socket created successfully!" << std::endl;

    // 关闭socket
    close(sockfd);

    return 0;

}

运行指令:

在ubantu系统中(windows系统无法运行),先切换到代码文件所在路径,例如:文件名为socked.cpp,文件路径为/home/socket_learn/socked.cpp

cd /home/socket_learn

然后运行g++ -o sockfd sockfd.cpp 这条指令使用 GNU C++ 编译器(g++)来编译 sockfd.cpp 源代码文件,并生成一个名为 sockfd 的可执行文件。

g++ -o sockfd sockfd.cpp

最后运行

./sockfd

代码运行结果:

Socket created successfully!

说明

socket():首先创建一个 IPv4 (AF_INET)、面向连接的 TCP (SOCK_STREAM) 套接字。

close():关闭套接字,释放资源。

错误处理

  • 如果 socket() 返回 -1,则表示套接字创建失败,可以通过 errno 获取错误码,使用 strerror(errno) 打印错误信息。
  • 常见错误:
    • EACCES:权限问题,无法创建套接字。
    • ENFILE:系统中打开的文件(套接字)已达到上限。
    • EMFILE:进程中打开的文件(套接字)已达到上限。

总结

  • socket() 是 C++ 网络编程的基础,用于创建套接字。
  • 套接字可用于 TCP 和 UDP 等协议,创建后需要结合 connect()bind()send()recv() 等函数来实现通信。

2.绑定IP地址和端口:bind()

在网络编程中,bind() 函数用于将创建的套接字绑定到一个特定的IP地址和端口号。它通常用于服务器端程序,允许服务器在特定的地址和端口上监听客户端的连接请求。

bind()函数的语法

#include <sys/types.h>
#include <sys/socket.h>

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

参数说明

  1. sockfd:由 socket() 函数返回的套接字文件描述符。
  2. addr:指向 sockaddr 结构体的指针,包含IP地址和端口号信息。
  3. addrlenaddr 结构体的大小,通常使用 sizeof(struct sockaddr_in)

sockaddr_in结构体

在绑定 IPv4 地址时,sockaddr_in 结构体通常用于表示IP地址和端口号。该结构体定义在 <netinet/in.h> 中,格式如下:

struct sockaddr_in {
    sa_family_t    sin_family;   // 地址族(AF_INET 表示 IPv4)
    in_port_t      sin_port;     // 端口号(需要使用 htons() 转换成网络字节序)
    struct in_addr sin_addr;     // IP 地址(使用 inet_addr() 或 INADDR_ANY)
};
  • sin_family:地址族,通常设置为 AF_INET(表示 IPv4)。
  • sin_port:端口号,必须使用 htons() 函数将端口号从主机字节序转换为网络字节序。
  • sin_addr:IP地址,使用 inet_addr() 函数将字符串形式的IP地址转换为 in_addr 类型,或者使用 INADDR_ANY 表示绑定到本地所有可用的网络接口。

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno 以指示错误原因。

常见错误

  • EADDRINUSE:指定的IP地址或端口已经被占用。
  • EINVAL:套接字已经绑定过一次,不能重复绑定。
  • EBADF:提供的文件描述符无效。

示例代码

以下示例演示如何在服务器端创建一个套接字,并将其绑定到本地IP地址和端口号 8080

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h> // 包含 sockaddr_in 结构体定义
#include <arpa/inet.h>  // 包含 htons、inet_addr 等函数
#include <unistd.h>     // 包含 close 函数
#include <cstring>      // 包含 memset 函数

int main() {
    // 1. 创建套接字 (IPv4, TCP)
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) {
        std::cerr << "Failed to create socket. Error: " << strerror(errno) << std::endl;
        return -1;
    }

    // 2. 定义服务器地址结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));  // 将结构体清零
    server_addr.sin_family = AF_INET;              // IPv4
    server_addr.sin_port = htons(8080);            // 设置端口号,使用 htons 转换为网络字节序
    server_addr.sin_addr.s_addr = INADDR_ANY;      // 绑定到本地所有可用地址

    // 3. 绑定套接字到IP地址和端口
    if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "Failed to bind. Error: " << strerror(errno) << std::endl;
        close(sockfd);  // 关闭套接字
        return -1;
    }

    std::cout << "Bind successful. Socket is now bound to port 8080." << std::endl;

    // 4. 关闭套接字
    close(sockfd);

    return 0;
}

运行结果:

Bind successful. Socket is now bound to port 8080.

代码说明

socket():创建了一个 IPv4、TCP 套接字。

sockaddr_in 结构体:用于存储服务器的IP地址和端口信息。INADDR_ANY 表示服务器可以监听来自所有本地网络接口的连接。

bind():将套接字与指定的IP地址和端口绑定。如果绑定成功,套接字就可以在指定的端口上接收客户端的连接请求。

htons():将端口号从主机字节序转换为网络字节序,这是必要的步骤,因为不同的系统可能使用不同的字节序。

close():关闭套接字,释放资源。

如何处理多网络接口

如果服务器主机有多个网络接口(多个IP地址),可以使用不同的IP地址进行绑定:

  • 绑定到特定IP地址:将 server_addr.sin_addr.s_addr 设置为特定的IP地址(如 inet_addr("192.168.1.100"))。
  • 绑定到所有接口:使用 INADDR_ANY,这意味着套接字将绑定到主机的所有可用网络接口,可以接受来自任意接口的连接。例如:以太网接口(Ethernet):物理网络接口,通过网线连接到局域网。Wi-Fi 接口:无线网络接口,通过 Wi-Fi 连接到网络。环回接口(Loopback):通常是 127.0.0.1,用于本地程序之间的通信(即不经过网络,只在本机内部进行通信)。

绑定后的后续操作

通常,服务器端程序在成功绑定套接字后,会执行以下步骤:

  1. 监听连接请求:使用 listen() 函数开始监听来自客户端的连接请求。
  2. 接受连接:使用 accept() 函数接受客户端的连接,并生成一个新的套接字用于与客户端通信。

小结

  • bind() 函数将套接字绑定到一个IP地址和端口号,用于服务器端程序监听来自客户端的连接。
  • sockaddr_in 结构体用于指定绑定的IP地址和端口号。
  • 绑定后,服务器可以使用 listen()accept() 函数与客户端进行通信。

3.监听连接请求:listen()

在网络编程中,服务器端需要监听客户端的连接请求,以便处理它们。listen() 函数就是用来让服务器开始监听连接请求的。

listen() 函数的作用:

listen() 函数的作用是将套接字设为被动模式,从而告诉操作系统这个套接字将用于接受连接。这个函数的主要功能是:

  1. 让服务器开始监听来自客户端的连接请求。
  2. 设置连接队列的最大长度,即客户端连接请求的等待队列。

listen() 的函数原型(以 C/C++ 为例):

int listen(int sockfd, int backlog);
  • sockfd:由 socket() 函数返回的套接字描述符,这个套接字已经通过 bind() 函数绑定到一个本地 IP 地址和端口号。
  • backlog:指定在处理客户端连接之前,内核允许的最大等待连接数。它表示在服务器开始处理请求前,客户端连接可以在队列中等待的数量上限。

使用 listen() 的步骤

(1)创建套接字:通过 socket() 函数创建套接字。

  (2)绑定 IP 地址和端口:通过 bind() 函数将套接字绑定到一个本地 IP 地址和端口号。

  (3)开始监听:调用 listen() 函数,开始监听客户端的连接请求。

代码示例(C/C++):

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

int main() {
    // 1. 创建套接字
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        std::cerr << "Failed to create socket\n";
        return -1;
    }

    // 2. 绑定 IP 地址和端口号
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;  // 使用 IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 绑定到所有可用的网络接口
    server_addr.sin_port = htons(8080);  // 绑定端口 8080

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "Bind failed\n";
        close(server_fd);
        return -1;
    }

    // 3. 开始监听,允许最多 10 个待处理连接
    if (listen(server_fd, 10) == -1) {
        std::cerr << "Listen failed\n";
        close(server_fd);
        return -1;
    }

    std::cout << "Server is listening on port 8080\n";

    // 4. 等待客户端连接(这里只是展示,未做实际 accept 操作)
    // ...

    // 关闭套接字
    close(server_fd);
    return 0;
}

参数说明:

  • server_fd:服务器套接字描述符,通过 socket() 函数创建。
  • 10backlog:最大连接等待队列长度。当多个客户端几乎同时尝试连接服务器时,服务器会把这些请求放在一个队列中,这个参数决定队列的大小。超过这个数量的连接请求将被拒绝,返回错误。

监听的实际含义

(1)被动模式listen() 函数将套接字转变为被动模式,被动模式意味着这个套接字将用于接受传入的连接,而不会主动向其他服务器发出连接请求。服务器将处于等待状态,直到有客户端请求连接。

(2)连接队列:当有多个客户端请求连接时,操作系统会把这些请求放在一个队列中。backlog 参数决定队列中最多可以有多少个未处理的连接请求。在队列满时,如果有新的连接请求,它们将被拒绝,客户端可能会收到错误信息。

队列的作用

  • 服务器处理每个客户端连接的速度可能不同,而客户端请求连接的速度可能较快。backlog 队列允许服务器有缓冲时间来处理请求。
  • 如果服务器忙于处理现有的连接,其他客户端的连接请求可以暂时存放在队列中等待处理。如果队列已满,额外的连接请求将被拒绝。

4.接受客户端连接:accept()

accept() 是服务器端网络编程中的一个关键函数,用于接受来自客户端的连接请求。服务器在调用 listen() 开始监听客户端的连接后,当有客户端尝试连接时,accept() 函数负责接受该连接并为此生成一个新的套接字以进行数据传输。

accept() 函数的作用

  • accept() 函数从服务器的等待队列中取出一个连接请求并与客户端建立连接。
  • 它为该连接分配一个新的套接字,该套接字将用于与客户端之间的实际通信。
  • 原来的监听套接字继续监听其他连接请求。

accept() 的函数原型(以 C/C++ 为例):

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd:服务器的监听套接字文件描述符(由 socket()bind() 创建并监听)。
  • addr:指向一个 sockaddr 结构体的指针,用于存储客户端的地址信息(客户端的 IP 和端口)。
  • addrlenaddr 结构体的大小,传入时是该结构体的大小,返回时表示客户端地址的实际大小。

返回值

  • 成功时,accept() 返回一个新的套接字文件描述符,这个描述符用于与客户端进行后续的通信。
  • 如果出错,返回值为 -1,同时设置 errno 以表示错误类型。

使用场景

accept() 通常和 socket()bind()listen() 函数一起使用。以下是服务器接受客户端连接的典型步骤:

  1. 创建套接字:使用 socket() 创建服务器端的套接字。
  2. 绑定 IP 和端口:使用 bind() 将套接字绑定到特定的 IP 地址和端口。
  3. 监听连接请求:使用 listen() 函数监听客户端连接请求。
  4. 接受连接:使用 accept() 接受客户端连接并生成一个新的套接字进行通信。

代码示例(C/C++):

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

int main() {
    // 1. 创建套接字
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        std::cerr << "Failed to create socket\n";
        return -1;
    }

    // 2. 绑定 IP 地址和端口号
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;  // 使用 IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY;  // 绑定到所有可用的网络接口
    server_addr.sin_port = htons(8080);  // 绑定端口 8080

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "Bind failed\n";
        close(server_fd);
        return -1;
    }

    // 3. 开始监听
    if (listen(server_fd, 10) == -1) {
        std::cerr << "Listen failed\n";
        close(server_fd);
        return -1;
    }

    std::cout << "Server is listening on port 8080\n";

    // 4. 接受客户端连接
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
    if (client_fd == -1) {
        std::cerr << "Accept failed\n";
        close(server_fd);
        return -1;
    }

    std::cout << "Connection accepted from " << inet_ntoa(client_addr.sin_addr)
              << ":" << ntohs(client_addr.sin_port) << "\n";

    // 与客户端通信代码(略)

    // 关闭客户端和服务器套接字
    close(client_fd);
    close(server_fd);

    return 0;
}

参数解释:

  • server_fd:由 socket() 函数创建的服务器监听套接字。
  • client_addr:这是一个 sockaddr_in 结构体,存储客户端的 IP 地址和端口号。
  • client_addr_len:表示 client_addr 结构体的大小。

accept() 的流程:

(1)阻塞行为accept() 是一个阻塞函数,意味着它会一直等待,直到有客户端连接请求到达。如果没有连接请求,服务器会在此函数上阻塞。

(2)返回客户端套接字:一旦连接请求到达,accept() 从连接队列中取出请求,返回一个新的套接字,用于与客户端进行通信。

(3)服务器继续监听:原来的监听套接字 (server_fd) 仍然处于监听状态,可以接受其他客户端连接请求,而当前客户端的通信使用新的套接字 (client_fd) 进行。

常见用法:

  • 服务器通过 accept() 获取与客户端通信的专用套接字。后续的数据接收和发送可以使用 read()write()recv()send()
  • 通信结束后,服务器需要调用 close() 关闭新的套接字以释放资源。

总结

  • accept() 用于从连接队列中取出客户端的连接请求,建立连接。
  • 返回一个新的套接字,用于服务器与客户端的通信。
  • 原监听套接字继续等待其他连接请求。

通过 accept(),服务器能够与多个客户端进行通信,每个客户端都有自己独立的套接字,而服务器的监听套接字则持续接收新的连接请求。

5.发送和接收数据:send()/recv()

在网络编程中,服务器和客户端建立连接后,可以通过 send()recv() 函数来发送和接收数据。这两个函数分别用于通过套接字向远程端发送数据和从远程端接收数据。

send() 函数

send() 函数用于通过套接字发送数据到连接的另一端。

send() 的函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd:套接字描述符,用于标识连接。
  • buf:指向存储待发送数据的缓冲区的指针。
  • len:要发送的数据长度(字节数)。
  • flags:发送选项,通常设置为 0。可以设置不同的标志来控制发送行为,例如 MSG_DONTWAIT(非阻塞发送)。
返回值:
  • 返回实际发送的字节数。如果返回值小于 len,表示数据未完全发送。
  • 如果返回 -1,表示发送失败,并设置 errno

recv() 函数

recv() 函数用于通过套接字从连接的另一端接收数据。

recv() 的函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd:套接字描述符,标识连接。
  • buf:指向接收数据的缓冲区的指针。
  • len:缓冲区的大小(即最多接收多少字节)。
  • flags:接收选项,通常设置为 0。可以设置不同的标志来控制接收行为,例如 MSG_WAITALL(等待所有数据)。
返回值:
  • 返回实际接收到的字节数。如果返回 0,表示连接已关闭。
  • 如果返回 -1,表示接收失败,并设置 errno

发送和接收数据的流程

  1. 服务器和客户端建立连接。
  2. 服务器使用 recv() 函数接收来自客户端的数据。
  3. 客户端使用 send() 函数向服务器发送数据,或反之。
  4. 在发送或接收完数据后,双方可以通过 close() 关闭套接字。

代码示例

以下是使用 send()recv() 进行数据发送和接收的代码示例:

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

int main() {
    // 1. 创建套接字
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd == -1) {
        std::cerr << "Failed to create socket\n";
        return -1;
    }

    // 2. 绑定 IP 地址和端口号
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);

    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "Bind failed\n";
        close(server_fd);
        return -1;
    }

    // 3. 开始监听
    if (listen(server_fd, 10) == -1) {
        std::cerr << "Listen failed\n";
        close(server_fd);
        return -1;
    }

    std::cout << "Server is listening on port 8080\n";

    // 4. 接受客户端连接
    struct sockaddr_in client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_addr_len);
    if (client_fd == -1) {
        std::cerr << "Accept failed\n";
        close(server_fd);
        return -1;
    }

    // 5. 接收数据
    char buffer[1024] = {0};
    ssize_t recv_len = recv(client_fd, buffer, sizeof(buffer), 0);
    if (recv_len == -1) {
        std::cerr << "Receive failed\n";
        close(client_fd);
        close(server_fd);
        return -1;
    }

    std::cout << "Received from client: " << buffer << std::endl;

    // 6. 发送数据
    const char *response = "Hello from server";
    if (send(client_fd, response, strlen(response), 0) == -1) {
        std::cerr << "Send failed\n";
    }

    // 7. 关闭套接字
    close(client_fd);
    close(server_fd);

    return 0;
}
服务端代码:
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    // 1. 创建套接字
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (client_fd == -1) {
        std::cerr << "Failed to create socket\n";
        return -1;
    }

    // 2. 连接到服务器
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) {
        std::cerr << "Connection failed\n";
        close(client_fd);
        return -1;
    }

    // 3. 发送数据
    const char *message = "Hello from client";
    if (send(client_fd, message, strlen(message), 0) == -1) {
        std::cerr << "Send failed\n";
    }

    // 4. 接收数据
    char buffer[1024] = {0};
    ssize_t recv_len = recv(client_fd, buffer, sizeof(buffer), 0);
    if (recv_len == -1) {
        std::cerr << "Receive failed\n";
        close(client_fd);
        return -1;
    }

    std::cout << "Received from server: " << buffer << std::endl;

    // 5. 关闭套接字
    close(client_fd);

    return 0;
}

关键点:

  1. 阻塞行为send()recv() 默认是阻塞的,意味着如果网络缓慢或没有数据到达,程序会等待。可以通过设置非阻塞模式改变这种行为。
  2. 返回值检查:检查 send()recv() 的返回值很重要,以确保数据正确发送和接收,并处理错误情况。
  3. 数据传输的长度send() 可能不会一次发送所有数据,程序需要处理这种情况,确保所有数据都被发送。
  4. 连接关闭:如果 recv() 返回 0,表示对方关闭了连接。

总结:

  • send() 用于通过套接字发送数据。
  • recv() 用于接收来自远程端的数据。
  • 它们是实现服务器与客户端通信的基础工具,通常与 socket()bind()connect() 等函数配合使用。

6.关闭连接:close()

在网络编程中,close() 函数用于关闭套接字并释放与该套接字相关的资源。当服务器或客户端不再需要与对方通信时,调用 close() 可以终止连接。

close() 函数的作用

close() 函数不仅仅是关闭文件描述符(在这种情况下是套接字),还会终止与该套接字关联的 TCP 连接,释放所有资源。如果套接字是连接的一部分(如 TCP 连接),则会通知对端连接已关闭,后续通信不再可能。

close() 函数的原型:

int close(int sockfd);
  • sockfd:套接字描述符,标识要关闭的连接。
返回值:
  • 返回 0 表示成功。
  • 返回 -1 表示失败,并设置 errno,以提供错误的具体信息。

关闭连接的流程

  1. 当服务器或客户端调用 close() 函数时,系统会开始执行 TCP 的四次挥手(Four-Way Handshake)协议,以优雅地关闭连接。
  2. 在这个过程中,系统会将套接字的状态从 ESTABLISHED(已建立连接)变为 FIN_WAIT 等不同状态,直到连接完全关闭。
  3. 调用 close() 后,系统会释放与该套接字关联的所有内存资源,包括文件描述符、缓冲区等。

客户端

1.创建套接字:socket()

2.连接到服务器:connect()

3.发送和接收数据:send()/recv()

4.关闭连接:close()

Socket广泛应用于各种网络通信场景,如HTTP、FTP、电子邮件等协议的底层实现。

示例:

服务端:

#include <iostream> 
#include <cstring>
#include <sys/socket.h>  //包含socket函数和数据结构
#include <netinet/in.h>  //包含Internet地址簇
#include <unistd.h>     //包含标准符号常数和类型

using namespace std;
const int PORT=9006;

int main(){
    int server_fd,new_socket;//定义套接字文件描述符
    /*server_fd表示服务器的文件描述符,在网络编程中,套接字(socket)通过文件描述符来操作,就像操作文件一样。
    new_socket表示新的连接套接字,当服务器接受客户端的连接请求时,它会创建一个新的套接字来处理这个连接,
    这个新的套接字通过 new_socket 变量来表示。*/
    struct sockaddr_in address;//定义地址结构体
    /*sockaddr_in 结构体是一个非常重要的数据结构,它用于存储网络地址信息。
    这个结构体是 sockaddr 结构体的一个特化版本,专门用于IPv4地址。在 <netinet/in.h> 头文件中定义*/
    int addrlen=sizeof(address);//地址长度
    char buffer[1024]={0};//定义缓冲区
    /*定义了一个大小为1024字节的字符数组,用于存储接收到的数据或将要发送的数据。{0} 初始化数组中的所有元素为0,
    这是一个常见的做法,用于确保缓冲区不包含任何随机数据。当然了,它还有:保证数据完整性,提高效率,数据格式化,
    防止数据丢失等功能,这里是简单示例,主要就是用于存储接收到的数据或将要发送的数据,就不做解释了,*/
    const char *message="你说得对!";//定义服务器发送的消息,使用 const 可以防止函数意外修改字符串内容。安全

    //创建套接字,用于在C++网络编程中创建一个TCP套接字的。
   /* if((server_fd=socket(AF_INET,SOCK_STREAM,0))==0){
        perror("socket failed");
        exit(EXIT_FAILURE);
    }*/
    if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    /*socket() 是一个系统调用,用于创建一个新的套接字。它返回一个文件描述符,
    该文件描述符是用于后续套接字操作(如绑定、监听、连接等)的索引。
    AF_INET表示IPv4地址族,用于创建一个基于IPv4的套接字。
    SOCK_STREAM表示创建一个提供序列化、可靠、双向连接的字节流套接字,这通常用于TCP连接。
    传递0表示使用默认的协议,对于AF_INET和SOCK_STREAM,这意味着使用TCP协议。
    == 0检查server_fd是否为0,即检查套接字是否创建失败。
    perror()函数将打印出错误信息到标准错误流
    exit()函数用于终止当前程序,并返回一个状态码给操作系统。EXIT_FAILURE是一个宏,通常定义为非零值,表示程序异常终止
*/

    //设置套接字选项,允许地址重用
    int opt=1;
    if(setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt))){
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    /*opt这个变量用于表示套接字选项的状态,1 表示启用该选项。
    setsockopt() 是一个系统调用,用于设置套接字选项。它可以改变套接字的行为,例如如何接收数据或者如何处理特殊的网络条件。
    SOL_SOCKET是一个指定套接字选项级别的常量,表示这是针对套接字本身的选项,而不是针对某个特定协议的。
    SO_REUSEADDR 是一个套接字选项,允许套接字绑定到一个已经被使用(在TIME_WAIT状态)的本地地址和端口。
    SO_REUSEPORT 是一个扩展选项,允许多个套接字绑定到同一个端口,只要它们的地址不同。
    如果 setsockopt() 返回非零值,表示设置选项失败。
    */

    //绑定套接字到端口
    address.sin_family=AF_INET;//地址簇,
    address.sin_addr.s_addr=INADDR_ANY;//地址。INADDR_ANY表示接受任意IP的连接
    address.sin_port=htons(PORT);//端口号,htons函数将主机字节序转换为网络字节序
    /*这三段代码是设置服务器端套接字地址信息的一部分,它们配置了服务器将监听的协议族、IP地址和端口号
    sin_family 是 sockaddr_in 结构体的一个成员,它指定了地址族。设置 sin_family 为 AF_INET 表示服务器将使用 IPv4 协议。
    sin_addr 是 sockaddr_in 结构体的一个成员,它是一个 in_addr 结构体,用于存储 IPv4 地址。
    INADDR_ANY 是一个特殊的常量,当绑定套接字时使用。它告诉操作系统自动绑定到所有可用的网络接口的 IPv4 地址。
    sin_port 是 sockaddr_in 结构体的一个成员,用于指定端口号。htons() 函数是 "host to network short" 的缩写,它将一个短整型(16位)从主机字节序转换为网络字节序。
    */

     //绑定套接字
    if(bind(server_fd,(struct sockaddr *)&address,sizeof(address))<0){
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    /*bind() 是一个系统调用,用于将一个套接字(server_fd)绑定到指定的地址和端口上。
    (struct sockaddr *)&address将address变量的地址传递给bind()函数
    其他基本的东西都和上面提到过的大相径庭
    */


    //监听套接字
    if(listen(server_fd,3)<0){//监听,参数3表示最大连接数
      perror("listen");
      exit(EXIT_FAILURE);
    }
    /*listen() 是一个系统调用,用于告诉内核准备接受连接请求。这个函数将一个被动套接字(也称为监听套接字)转变为监听状态。
    参数 3 表示服务器将允许最多3个连接请求在队列中等待接受。
    */

    cout<<"Listening on port"<<PORT<<endl;

    //接受客户端连接
    if((new_socket=accept(server_fd,(struct sockaddr*)&address,(socklen_t*)&addrlen))<0){
        perror("accept");
        exit(EXIT_FAILURE);
    }
    /*accept() 是一个系统调用,用于从监听队列中取出第一个连接请求,并为该请求创建一个新的套接字。*/
    
    //读取客户端发送的数据
    read(new_socket,buffer,1024);//读取数据到缓冲区
    cout<<"消息来自丽宝:"<<buffer<<endl;
    /*read() 是一个系统调用,用于从文件描述符(在这里是套接字)读取数据。
    new_socket是通过 accept() 函数创建的新套接字的文件描述符,用于与客户端进行通信*/

    //发送数据回客户端
    send(new_socket,message,strlen(message),0);//发送消息
    cout<<"消息发送成功\n";

    //关闭套接字
    close(new_socket);//关闭客户端套接字
    close(server_fd);//关闭服务器套接字
    return 0;
}

客户端:

#include <iostream>  
#include <sys/socket.h> // 包含socket函数和数据结构
#include <arpa/inet.h> // 包含inet函数,用于IP地址转换
#include <unistd.h>  // 包含标准符号常数和类型
#include <string.h>  

using namespace std;

const int PORT = 9006; // 定义服务器的端口号

int main() {
    int sock = 0; // 定义套接字文件描述符
    struct sockaddr_in serv_addr; // 定义服务器地址结构体
    const char *hello = "302女的美,男的帅!"; // 定义客户端发送的消息
    char buffer[1024] = {0}; // 定义接收数据的缓冲区
    /*sock 用于存储客户端套接字的文件描述符,它是网络通信中的一个重要概念,代表了网络通信的一端。*/
   

    // 创建套接字
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        cout << "\n Socket creation error \n";
        return -1;
    }
    /*如果创建成功,sock 将包含一个非负值;如果创建失败,则为 -1。*/

    serv_addr.sin_family = AF_INET; // 服务器地址族
    serv_addr.sin_port = htons(PORT); // 服务器端口号

    // 将文本形式的IP地址转换为二进制形式
    if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
        cout << "\nInvalid address / Address not supported \n";
        return -1;
    }
    /*inet_pton() 用于将表示网络地址的字符串转换为网络字节序的二进制形式。*/

    // 连接到服务器
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        cout << "\nConnection Failed \n";
        return -1;
    }
    /*connect() 用于客户端套接字与服务器套接字建立连接。*/

    // 发送数据
    send(sock, hello, strlen(hello), 0); // 发送消息
    cout << "消息发送成功:\n";

    // 接收服务器发送的数据
    read(sock, buffer, 1024); // 从服务器接收数据
    cout << "消息来自赟赟: " << buffer << std::endl;

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

运行指令:

g++ -o server server.cpp
g++ -o client client.cpp
./server
./client

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2126786.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

又一款强大好用的Shell脚本项目,支持Bash,Sh、Dash、Ksh等,甚至可以在编辑器中直接用,程序员必备!(附源码)

作为一个程序员&#xff0c;肯定经常都要和shell脚本打交道&#xff0c;Shell脚本可以帮我们自动化各种任务&#xff0c;但也经常有格式错误、拼写错误、逻辑错误等等麻烦&#xff0c;而且它不会告诉你错在哪里&#xff01; 今天就给大家分享一个超级实用的开源项目 - ShellCh…

【笔记】自动驾驶预测与决策规划_Part2_基于模型的预测方法

基于模型的预测方法 0. 前言1. 预测系统概述2. 定速度预测3.定曲率预测4. 短时预测与长时预测5. 基于手工特征的意图预测6: 基于模型的轨迹预测 0. 前言 本文主要记录课程《自动驾驶预测与决策技术》的学习过程&#xff0c;难免会有很多纰漏&#xff0c;感谢指正。 课程链接&am…

C++那些事之精选68道面试题

有小伙伴问我要一些C的学习题目/面试题目&#xff0c;我这里总结了一下分享给大家&#xff0c;大家可以自行去回答下面的问题&#xff0c;看看可以解答多少个。 注&#xff1a;懒人版附代码与答案会在文末星球详细讲解。 大纲 基础概念面向对象编程STL&#xff08;标准模板库&a…

如何加入PTP硬件时钟的组播组

首先要先判断当前机器是否能收到PTP时钟源发出的组播包 timeout 100 tcpdump -nni bond0 port 319 or port 320 -el -c 100抓包能抓到以下包信息&#xff0c;即能正常收到PTP时钟源发出的组播信息。其中的224.0.1.129即为组播地址 第一步正常了后&#xff0c;开始加入组播源 ip…

jmeter元件+取样器(十)

一、元件与组件 二、作用域 由于查看结果树与京东和百度是平级关系&#xff0c;所以均生效。 三、线程属性-取样器 1、请求方法&#xff08;在参数和消息体数据里填写请求数据&#xff0c;其结果可能会出现在URL或者请求体中&#xff0c;其请求头类型可能为表单或json格 &am…

解密Fiddler,从零开始轻松掌握弱网测试技巧!

使用Fiddler对手机App应用进行抓包&#xff0c;可以对App接口进行测试&#xff0c;也可以了解App传输中流量使用及请求响应情况&#xff0c;从而测试数据传输过程中流量使用的是否合理。这篇文章就带大家了解一下抓包过程。 01、Fiddler设置 1、启动Fiddler->Tools->Fid…

谷歌账号登录的时候需要手机验证,但是验证的手机号码已经注销了怎么办?

在服务的过程中&#xff0c;时不时会遇到有朋友问&#xff0c;自己的谷歌账号在登录的时候需要验证&#xff0c;这个账号绑定到的手机号已经注销了&#xff0c;怎么办&#xff1f;账号还能找回来吗&#xff1f; 今天GG账号服务就来为大家解答这个问题&#xff0c;让大家少走弯…

GEE Shapefile 格式转换 GeoJSON

在地理信息系统&#xff08;GIS&#xff09;领域&#xff0c;数据格式之间的转换是一项常见的需求。例如&#xff0c;将 Shapefile 格式转换为 GeoJSON 格式&#xff0c;对于上传数据到 Google Earth Engine (GEE) 尤其有用。本文将通过一个 Python 脚本的示例&#xff0c;实现…

软件无线电1-MATLAB实现FM调制解调

1、MATLAB读取语音文件 准备一段wav的语音文件&#xff0c;我用笔记本自带的录音机录制了一段自己的语音“爱福皮的姐.wav“&#xff0c;MATLAB读取语音文件&#xff0c;并获取采样率信息。 clc; clear all; %% ***************read file************************************…

【困难】 猿人学web第一届 第18题 jsvmp 洞察先机

文章目录 数据接口分析还原加密参数插桩调试分析日志插桩补充 python 代码 数据接口分析 数据接口 https://match.yuanrenxue.cn/match/18data 请求参数 {page: 页码, t: 时间戳, v: 加密值} 请求第一页不需要携带 t, v 参数 cookie 只需要携带 sessionid 只要 还原加密字段…

深入理解Java反射技术及其应用

什么是反射技术&#xff1f; Java反射机制是一种强大的特性&#xff0c;它允许程序在运行时动态加载类并获取类或对象的属性和方法。其核心在于JVM通过获得class对象进行反编译&#xff0c;从而获取对象的各种信息。 反射机制的基本特点 动态性 Java是一种先编译后运行的语言…

【电子通识】规格书上的%FS和%RD具体指什么?

在仪器仪表类的手册上&#xff0c;常见的精度表达规格显示方式&#xff1a;%FS 和%RD 究竟如何解读呢&#xff1f; 术语解说 %RD(Reading)&#xff1a;用于表示对比显示值(读值)存在多少(%)的误差 %FS(Full Scale)&#xff1a;用于表示对比全量程存在多少(%)的误差 %SP(Set Poi…

基于vue框架的城市体育运动交流平台15s43(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;用户,赛事类型,近期赛事,比赛报名,器材类型,器材信息,自由约战,运动队伍 开题报告内容 基于Vue框架的城市体育运动交流平台开题报告 一、项目背景与意义 随着城市化进程的加速和居民健康意识的提升&#xff0c;城市体育运动已成为现代…

思维导图神器!四款高效工具助你职场逆袭

在如今这个信息爆炸的时代&#xff0c;如何高效地整理和呈现思维&#xff0c;成为了一个重要的能力&#xff1b;思维导图作为一种有效的思维工具&#xff0c;被广泛应用于工作、学习和生活中&#xff1b;今天&#xff0c;我将为大家介绍四款常用的思维导图软件并分享一下它们的…

win10任务栏透明如何调整?——详解Windows任务栏设置与优化技巧

在这个数字化时代&#xff0c;电脑已经成为我们日常办公和生活中的。关于win10任务栏透明的设置方法&#xff0c;身边很多同事都在咨询。 本文就来简单介绍下关于电脑任务栏个性化设置的方法&#xff0c;毕竟任务栏影响着用户体验。这时&#xff0c;一款优秀的任务栏优化工具就…

模拟实现string类: clear函数、流提取(<<)和流插入(>>)运算符重载、>、<、==、<=、>=、!=的运算符重载、赋值运算符(=)重载等的介绍

文章目录 前言一、 clear函数二、流提取(<<)和流插入(>>)运算符重载三、 >、<、、<、>、!的运算符重载四、赋值运算符&#xff08;&#xff09;重载总结 前言 模拟实现string类: clear函数、流提取(<<)和流插入(>>)运算符重载、>、<…

[论文笔记]ChatQA: Surpassing GPT-4 on Conversational QA and RAG

引言 今天来看一下上篇论文笔记中反复介绍的 ChatQA: Surpassing GPT-4 on Conversational QA and RAG。 为了简单&#xff0c;下文中以翻译的口吻记录&#xff0c;比如替换"作者"为"我们"。 我们介绍了 ChatQA&#xff0c;这是一个模型套件&#xff0c;一…

机器学习和深度学习的常见概念总结(面试用,多原创图和公式)

目录 使用说明一、未分类损失函数&#xff08;Loss Function&#xff09;1. **损失函数的作用**2. **常见的损失函数**2.1. **均方误差&#xff08;MSE, Mean Squared Error&#xff09;**2.2. **均方根误差&#xff08;RMSE, Root Mean Squared Error&#xff09;**2.3. **平均…

1. 运动控制指令概要(omron 机器自动化控制器)

机器自动化控制器——第一章 运动控制指令概要 1-1 运动控制指令PLCopen运动控制用功能块运动控制指令概要▶ 运动控制指令的种类▶ 状态变化▶ 运动控制指令的启动和状态▶ 异常处理▶ 执行运动控制指令时输入变量的变更(指令重启)▶ 通过选择缓存模式执行指令多重启动▶ 通过…

四川锦程消费金融有限责任公司2024年(第一批次)催收机构选型入库采购公告

四川锦程消费金融有限责任公司2024年&#xff08;第一批次&#xff09;催收机构选型入库采购公告 根据需要&#xff0c; 四川锦程消费金融有限责任公司决定对外公 开选型采购催收代理合作的催收机构&#xff0c;欢迎符合条件的催收 机构参与采购。具体公告如下&#xff1a; 一…