socket通讯原理及例程(详解)
里面有疑问或者不正确的地方可以给我留言。
对TCP/IP、UDP、Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵。那么我想问:
- 什么是TCP/IP、UDP?
- Socket在哪里呢?
- Socket是什么呢?
- 你会使用它们吗?
什么是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 的工作原理基于“客户端-服务器”模型:
-
服务器端:
- 服务器程序在特定的IP地址和端口上“监听”(等待连接请求)。
- 当客户端请求连接时,服务器会接受连接,双方通过Socket进行数据传输。
-
客户端:
- 客户端程序向服务器发起连接请求,通过服务器的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);
参数说明
sockfd
:由socket()
函数返回的套接字文件描述符。addr
:指向sockaddr
结构体的指针,包含IP地址和端口号信息。addrlen
:addr
结构体的大小,通常使用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
,用于本地程序之间的通信(即不经过网络,只在本机内部进行通信)。
绑定后的后续操作
通常,服务器端程序在成功绑定套接字后,会执行以下步骤:
- 监听连接请求:使用
listen()
函数开始监听来自客户端的连接请求。 - 接受连接:使用
accept()
函数接受客户端的连接,并生成一个新的套接字用于与客户端通信。
小结
bind()
函数将套接字绑定到一个IP地址和端口号,用于服务器端程序监听来自客户端的连接。sockaddr_in
结构体用于指定绑定的IP地址和端口号。- 绑定后,服务器可以使用
listen()
和accept()
函数与客户端进行通信。
3.监听连接请求:listen()
在网络编程中,服务器端需要监听客户端的连接请求,以便处理它们。listen()
函数就是用来让服务器开始监听连接请求的。
listen()
函数的作用:
listen()
函数的作用是将套接字设为被动模式,从而告诉操作系统这个套接字将用于接受连接。这个函数的主要功能是:
- 让服务器开始监听来自客户端的连接请求。
- 设置连接队列的最大长度,即客户端连接请求的等待队列。
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()
函数创建。10
(backlog
):最大连接等待队列长度。当多个客户端几乎同时尝试连接服务器时,服务器会把这些请求放在一个队列中,这个参数决定队列的大小。超过这个数量的连接请求将被拒绝,返回错误。
监听的实际含义
(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 和端口)。addrlen
:addr
结构体的大小,传入时是该结构体的大小,返回时表示客户端地址的实际大小。
返回值
- 成功时,
accept()
返回一个新的套接字文件描述符,这个描述符用于与客户端进行后续的通信。 - 如果出错,返回值为
-1
,同时设置errno
以表示错误类型。
使用场景
accept()
通常和 socket()
、bind()
、listen()
函数一起使用。以下是服务器接受客户端连接的典型步骤:
- 创建套接字:使用
socket()
创建服务器端的套接字。 - 绑定 IP 和端口:使用
bind()
将套接字绑定到特定的 IP 地址和端口。 - 监听连接请求:使用
listen()
函数监听客户端连接请求。 - 接受连接:使用
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
。
发送和接收数据的流程
- 服务器和客户端建立连接。
- 服务器使用
recv()
函数接收来自客户端的数据。 - 客户端使用
send()
函数向服务器发送数据,或反之。 - 在发送或接收完数据后,双方可以通过
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;
}
关键点:
- 阻塞行为:
send()
和recv()
默认是阻塞的,意味着如果网络缓慢或没有数据到达,程序会等待。可以通过设置非阻塞模式改变这种行为。 - 返回值检查:检查
send()
和recv()
的返回值很重要,以确保数据正确发送和接收,并处理错误情况。 - 数据传输的长度:
send()
可能不会一次发送所有数据,程序需要处理这种情况,确保所有数据都被发送。 - 连接关闭:如果
recv()
返回 0,表示对方关闭了连接。
总结:
send()
用于通过套接字发送数据。recv()
用于接收来自远程端的数据。- 它们是实现服务器与客户端通信的基础工具,通常与
socket()
、bind()
、connect()
等函数配合使用。
6.关闭连接:close()
在网络编程中,close()
函数用于关闭套接字并释放与该套接字相关的资源。当服务器或客户端不再需要与对方通信时,调用 close()
可以终止连接。
close()
函数的作用
close()
函数不仅仅是关闭文件描述符(在这种情况下是套接字),还会终止与该套接字关联的 TCP 连接,释放所有资源。如果套接字是连接的一部分(如 TCP 连接),则会通知对端连接已关闭,后续通信不再可能。
close()
函数的原型:
int close(int sockfd);
sockfd
:套接字描述符,标识要关闭的连接。
返回值:
- 返回
0
表示成功。 - 返回
-1
表示失败,并设置errno
,以提供错误的具体信息。
关闭连接的流程
- 当服务器或客户端调用
close()
函数时,系统会开始执行 TCP 的四次挥手(Four-Way Handshake)协议,以优雅地关闭连接。 - 在这个过程中,系统会将套接字的状态从 ESTABLISHED(已建立连接)变为 FIN_WAIT 等不同状态,直到连接完全关闭。
- 调用
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