如果你想和远方的朋友通电话,但是,没有办法直接把自己的声音放在电线上变成电流信号,你需要使用电话机拿起听筒拨号,而这个电话就是Socket,它让你简单方便地完成电流通话,从我们编程的角度来看,我们直接使用TCP传输信息,需要考虑的东西太多了,而socket替我们封装实现了TCP,我们只需要使用socket的API,即可间接完成TCP通信。
文章目录
- 一、什么是socket ?
- 二、网络通信三要素
- 三、Socket和TCP的关系
- 四、如何进行Socket编程(流程)
- 五、TCP服务端的实现
- 六、Socket函数
- 1.socket()函数原型
- socket()函数参数举例:
- 常见错误:
- 2.setsockopt()函数
- 1. 函数原型与参数
- 2. 常用选项与功能
- 3. 典型应用场景
- UDP协议选项总结
- 1. SOL_SOCKET层选项(通用选项)
- 2. IPPROTO_UDP层选项(UDP协议特有)
- 3. 典型应用场景
- 3.关于sockaddr_in结构体
- 4.bind()函数
- 5.listen()监听函数
- 6.accept()函数
- 7.recv()函数
- 8.send()函数
- 9.close()函数
- 七、TCP客户端的实现
一、什么是socket ?
Socket是网络通信的“接口”,也就是程序与网路之间的中介。它为开发者提供了一套通用的函数或API,允许程序通过网络发送和接收数据。具体地说,socket抽象了网络底层的细节。比如数据包的传输、协议选择、错误处理等,开发者只需要使用socket提供的函数来建立连接和传输数据,而不需要关心网络底层的实现
- socket的特点
二、网络通信三要素
socket又称网络套接字
三、Socket和TCP的关系
Socket可以被视为对网络协议的封装或使用协议的工具。它提供了一组API,让开发者能够在应用层与网络协议(如TCP或UDP)进行交互。通过Socket,开发者可以方便地创建链接、发送和接收数据,而不需要直接处理底层协议的细节。因此,Socket实在应用程序和网络之间的一个抽象层。
简单的来说就是socket是一个网络编程函数,这个函数根据传入参数不同,就可以支持对应的协议通讯。。
四、如何进行Socket编程(流程)
五、TCP服务端的实现
基于Linux系统的服务端实现(请在Linux系统下进行部署)
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>using namespace std;int main()
{int server_fd,new_socket; //声明服务器socket和新的客户端socketstruct sockaddr_in address; //声明用于存储地址信息的结构体int opt = 1; //用于设置socket选项socklen_t addrlen = sizeof(address); //地址结构体的大小const int PORT = 8888; //服务器监听的端口号//第一步//创建服务器socketserver_fd=socket(AF_INET,SOCK_STREAM,0); //创建TCP流式socketif(server_fd==0) //创建失败则退出{cerr<<"Failed to create socket"<<endl;return 1; //退出程序}//设置socket选项,允许重用地址(可选)setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));//设置服务其地址信息address.sin_family=AF_INET; //IPv4地址族address.sin_addr.s_addr=INADDR_ANY; //允许接收来自任何IP地址(此处指的是本地服务)address.sin_port=htons(PORT); //端口号//第二步 绑定//将socket绑定到指定的地址和端口if(bind(server_fd,(struct sockaddr*)&address,sizeof(address))<0){cerr<<"Failed to bind"<<endl; //输出绑定失败的原因return 1; //退出程序}//第三步 监听//开始监听传入的连接请求,队列长度为3if(listen(server_fd,3)<0){cerr<<"Failed to listen"<<endl;return 1; //退出程序}cout<<"Server is listening on port "<<PORT<<endl;//无限循环,接受客户端的连接while(true){//第四步: 接受客户端的连接请求//接受客户端的连接请求new_socket=accept(server_fd,(struct sockaddr*)&address,(socklen_t*)addrlen);if(new_socket<0){//出错则跳过cerr<<"Failed to accept"<<endl;//输出连接失败的原因continue;; //跳过出错的连接}//接受客户端发送的消息char buffer[1024];//创建缓冲区//第五步: 读取信息ssize_t bytes_read=recv(new_socket,buffer,sizeof(buffer),0);//接收消息if(bytes_read>0) //处理接收到的消息{//输出接收到的消息cout<<"Received message: "<<buffer<<endl;//第六步: 发送信息const char* message="Hello from server"; //响应消息send(new_socket,message,strlen(message),0);//发送消息}//关闭连接close(new_socket);}//关闭服务器socketclose(server_fd);return 0;
}
六、Socket函数
socket()函数是网络编程中创建套接字的基础函数,它用于创建一个套接字并返回文件描述符,该描述符可以用于后续的网络通信操作。
1.socket()函数原型
int socket(int domain,int type,int protocol);
···返回值
成功时:返回套接字的文件描述符(一个整数,表示套接字),后续通过这个文件描述符来操作该套接字。
失败时:返回-1,并设置error来指示具体错误原因。
socket()函数参数举例:
常见错误:
2.setsockopt()函数
在TCP网络编程中,setsockopt()是一个常用的函数,用于配置Socket的一些行为或参数
- setsockopt()起什么作用?
setsockopt(server_fd,SOL_SOCKET,SO_REUSEADDR,&&opt,sizeof(opt));
- settsockopt():这个函数的作用是设置一个套接字的选项。其函数原型如下:
int setsockopt(int sockfd,int level,int optname,const void*optval,socklen_t optlen);
- server_fd:这是之前通过socket()创建的服务器套接字的文件描述符。
- SOL_SOCKET:表示要操作的级别是套接字本身(而不是某个特定协议的选项)。- SOL_SOCKET:是用于通用套接字选项的常量。
- SO_REUSEADDR :这个选项允许重用本地地址。在关闭套接字后,TCP会等待一段时间(TIME_WAIT状态)才能重新使用相同的端口和地址。如果你设置了SO_REUSEADDR,即使上一个连接还没有完全释放(处于TIME_WAIT状态),你也可以立即绑定相同的地址和端口,避免绑定失败。
- &opt:opt是一个整型变量,通常设置为1,表示启用SO_REUSEADDR选项。&opt是指向该变量指针,用于传递给setsockopt()。
- sizeof(opt):表示opt变量的大小,通常是4个字节(整数)。
1. 函数原型与参数
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd
:套接字描述符。level
:选项的协议层,常用值包括:SOL_SOCKET
(套接字通用选项)IPPROTO_TCP
(TCP协议选项)IPPROTO_IP
(IPv4协议选项)。
optname
:具体选项名称(如SO_REUSEADDR
)。optval
:指向选项值的指针(类型因选项而异)。optlen
:选项值的长度。
2. 常用选项与功能
(1)套接字通用选项(SOL_SOCKET
层)
选项名 | 功能说明 | 数据类型 |
---|---|---|
SO_REUSEADDR | 允许重用本地地址和端口(避免 TIME_WAIT 状态阻塞) | int |
SO_KEEPALIVE | 启用 TCP 保活机制(检测连接存活) | int |
SO_RCVBUF /SO_SNDBUF | 设置接收/发送缓冲区大小(需注意内核可能自动调整) | int |
SO_RCVTIMEO /SO_SNDTIMEO | 设置接收/发送超时时间(需 struct timeval 结构体) | struct timeval |
SO_LINGER | 延迟关闭连接(控制 close() 行为,如优雅关闭) | struct linger |
SO_BROADCAST | 允许 UDP 发送广播数据 | int |
SO_DONTROUTE | 禁用路由查找(仅通过本地接口发送) | int |
(2)TCP 协议选项(IPPROTO_TCP
层)
TCP_NODELAY
:禁用 Nagle 算法(减少小数据包延迟)。
3. 典型应用场景
-
地址复用
int reuse = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int));
需在
bind()
前调用。 -
超时控制
struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; // 5秒超时 setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
-
优雅关闭连接
struct linger lin = { .l_onoff = 1, .l_linger = 30 }; // 延迟30秒关闭 setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &lin, sizeof(lin));
-
优化性能
int buf_size = 64 * 1024; // 64KB缓冲区 setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(int));
UDP协议选项总结
UDP协议在网络编程中通过setsockopt()
函数设置选项,主要涉及SOL_SOCKET(通用套接字层)和IPPROTO_UDP(UDP协议层)两类选项。以下是关键选项及其应用场景:
1. SOL_SOCKET层选项(通用选项)
选项名 | 功能说明 | 数据类型 | 示例代码 |
---|---|---|---|
SO_BROADCAST | 允许UDP发送广播数据包(需目标地址为广播地址,如255.255.255.255) | int | 83 _ 31 |
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(int));
| SO_REUSEADDR | 允许多个套接字绑定到同一端口,适用于UDP多播或快速重启服务 | int
|
int reuse = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int));
| SO_RCVBUF/SO_SNDBUF | 设置接收/发送缓冲区大小(需注意内核可能自动调整) | int
|
int buf_size = 65536;
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &buf_size, sizeof(int));
| SO_DONTROUTE | 禁止路由查找,数据包直接通过本地接口发送(适用于本地网络调试) | int
|
int dontroute = 1;
setsockopt(sockfd, SOL_SOCKET, SO_DONTROUTE, &dontroute, sizeof(int));
2. IPPROTO_UDP层选项(UDP协议特有)
选项名 | 功能说明 | 数据类型 | 示例代码 |
---|---|---|---|
UDP_NOCHECKSUM | 禁用UDP校验和计算(发送时校验和设为0),适用于低可靠性需求场景 | int (布尔值) | 24 |
int nochecksum = 1;
setsockopt(sockfd, IPPROTO_UDP, UDP_NOCHECKSUM, &nochecksum, sizeof(int));
| UDP_CHECKSUM_COVERAGE | 强制启用UDP校验和计算(默认通常启用) | int
(布尔值) |
int checksum = 1;
setsockopt(sockfd, IPPROTO_UDP, UDP_CHECKSUM_COVERAGE, &checksum, sizeof(int));
| UDP_RECV_MAX_COALESCED_SIZE | 设置接收时合并数据包的最大字节数,优化接收效率(需系统支持) | DWORD
|
DWORD max_size = 4096;
setsockopt(sockfd, IPPROTO_UDP, UDP_RECV_MAX_COALESCED_SIZE, &max_size, sizeof(DWORD));
3. 典型应用场景
- 广播通信
- 启用
SO_BROADCAST
后,UDP可向局域网广播地址发送数据包,适用于设备发现或群发消息。
- 启用
- 多播(组播)支持
- 结合
IP_ADD_MEMBERSHIP
选项,加入多播组接收数据(需IP层设置)31。
- 结合
- 高性能传输优化
- 增大缓冲区(
SO_RCVBUF
)减少丢包,或禁用校验和(UDP_NOCHECKSUM
)降低CPU开销。
- 增大缓冲区(
- 本地调试与测试
- 使用
SO_DONTROUTE
避免数据包经路由转发,直接通过指定接口发送。
- 使用
3.关于sockaddr_in结构体
sockaddr_in是用于处理IPv4地址的结构体,它用于存储网络套接字(Socket)的地址信息,通常在使用bind()、connect()、accept()等网络相关函数时会用到。
sockaddr_in是sockaddr结构体的一个专用版本,它更方便地处理IPv4地址,而sockaddr是通用的套接字地址结构体。由于网络函数通常要求参数类型是sockaddr,我们会将sockaddr_in类型强制转换为sockaddr来使用
为什么不直接用sockaddr这个结构体呢?
因为sockaddr是通用的,但是这个 不易用
- sockaddr_in的结构体定义
在头文件<netinet/in.h>中,sockaddr_in结构体的定义如下
struct sockaddr_in{sa_family_t sin_family; //协议族(Address family),即协议类型in_port_t sin_port; //16位的端口号,网络字节序(大端序)struct in_addr sin_addr; //32位的IPv4地址char sin_zero[8]; //填充字段,保持与sockaddr结构体的大小一致
}
- 1.sin_family
该字段指定了地址族,必须设置为AF_INET,表示使用IPv4协议族(Internet Protocol version 4)
address.sin_family=AF_INET;
- 2.sin_port
该字段存储16位的端口号,表示与服务器或客户端进行通信的端口号。它必须使用网络字节序(大端序)来存储,因此在代码中经常会使用htons()函数来将主机字节序转换为网络字节序
address.sin_port=htons(8080);//将端口号8080转换为网络字节序
- 3.sin_addr
这是存储IPv4地址的结构体,使用的是struct in_addr类型。它实际上是一个32位的整数,通常通过inet_addr()函数来将点分十进制格式的IP地址转换为这个字段可以接收的格式
address.sin_addr.s_addr=inet_addr(“127.0.0.1”);//将点分十进制的IP地址转换为网络字节序
-
4.sin_zero
-
该字段是一个长度为8字节的填充字段,其作用是使sockaddr_in的大小与sockaddr结构体保持一致,这个字段通常不被使用,直接填充为0即可。
-
5.sockaddr_in和sockaddr的关系
sockaddr_in是用于IPv4的转用结构体,而网络函数(如bing()、connect())需要传递的参数类型是通用的sockaddr结构体,为了兼容,通常我们会将sockaddr_in强制转换为sockaddr类型
bind(server_fd,(struct sockaddr*)*address,szeof(address));
在上面的服务器代码中,address是sockaddr_in类型的变量,但在bind()函数中我们强制转换为sockaddr*来使用。
4.bind()函数
bind()函数的主要作用是将套接字与特定的本地地址(IP地址)和端口号绑定起来,使得该套接字可以通过指定的地址和端口接收数据。bind()函数通常用于服务器的套接字编程,以便为套接字分配一个固定的本地地址和端口。
- 函数原型
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
- sockfd:套接字描述符,由socket()函数返回。表示需要绑定的套接字。
- addr:指向sockaddr结构体的指针,包含要绑定的IP地址和端口信息。
- addrlen:addr的大小,同工厂为sizeof(struct sockaddr_in)。
- 参数详解
- sockfd:套接字描述符,由socket()函数返回。表示需要绑定的套接字。bind()函数使用这个套接字和本地地址进行绑定操作。
- addr:指向sockaddr结构体的指针,包含要绑定的IP地址和端口信息。大多数情况下,传递的是sockaddr_in结构体(IPv4)或者sockaddr_in6结构体(IPv6)的地址,并通过强制类型转换为sockaddr结构体。
- sockaddr_in结构体:
struct sockaddr_in{sa_family_t sin_family; //协议族(Address family),即协议类型in_port_t sin_port; //16位的端口号,网络字节序(大端序)struct in_addr sin_addr; //32位的IPv4地址char sin_zero[8]; //填充字段,保持与sockaddr结构体的大小一致
}
- addrlen:该参数指定地址的长度,通常为sizeof(struct sockaddr_in)或者(sizeof(struct sockaddr_in6)),具体取决于使用的是IPv4还是IPv6地址。
- 返回值
- 成功时返回0
- 失败时返回-1,并设置error以指示错误类型。常见的错误类型包括:
-
- EADDRINUSE:指定的地址以及被使用,通常是端口号冲突。
-
- EADDRNOTAVAIL:指定的IP地址不可用。
-
- EBADF:无效的文件描述符,通常是传递的sockfd非法。
-
- EINVAL:套接字已经绑定,不能重复绑定。
-
- ENOTSOCK:sockfd不是一个套接字。
- bind的作用
bind()函数通常用于服务器套接字编程,起作用是将套接字绑定到一个本地地址和端口上。具体来说,bind()会将该套接字关联到以下三项信息:
-
- (1)本地IP地址:表示该套接字从哪个本地网络接口接收数据。常见的绑定选项:INADDR_ANY:绑定到本地机器的所有网络接口,表示任何网络接口上发往这个端口的数据都将被接收。具体的IP地址:可以绑定一个特定的IP地址,如:“192.168.1.1”。
-
- (2)本地端口:表示该套接字将监听的端口号。服务器通常会绑定到一个特定的端口来等待客户连接。
-
- (3)协议族:治党使用的协议族(IPV4或IPv6).。通过设置sin_family为AF_INET或AF_INET6,来分别表示使用IPv4或IPv6.
- 为什么使用bind()?
-
服务器段:
在服务器端,bind()函数必须调用,它将套接字绑定到一个特定的端口和IP地址,使得客户端能够通过该地址和端口连接到服务器,如果不调用bind(),操作系统可能会自动分配一个临时的本地端口,这对于服务器来说是不可接受的,因为服务器需要一个固定的端口号以供客户端连接。 -
客户端
在客户端编程中,bind()通常不是必需的,客户端套接字一般依赖于系统自动分配的本地端口,以便于服务器通信。如果有特殊需求(比如希望客户端使用特定的本地IP地址和端口),也可以调用bind()绑定。
5.listen()监听函数
listen()函数在服务器端网络编程中用于将一个套接字设置为监听状态,使其能够接收来接客户端的连接请求。它是服务器端TCP套接字生命周期中一个关键步骤,用于处理被动套接字(即等待客户端连接的套接字)。
- 函数原型
int listen(int sockfd,int backlog);//
- 参数详解
-
- sockfd:套接字文件描述符
-
- backlog:(连接队列的最大长度)
- backlog参数指定内核为套接字维护的已完成连接队列和未完成连接队列的总长度。这个值表示最大可以同时等待处理的客户端连接请求数量
- 已完成连接队列:保存已经完成TCP三次握手的连接。
- 未完成连接队列:保存正在等待完成三次握手的连接。
- 当队列满了之后,如果有新的连接请求到达,它们会被拒绝,客户端将会收到ECONNREFUSED错误。
- 值可以根据服务器的预期负载进行设置。例如,高并发服务器可能设置较大的backlog值。
-
- 返回值:成功时返回0;失败返回-1,并设置errno来指示错误。
- listen()函数的功能和工作流程
- (1)将套接字从主动状态转换为被动状态
-
- 调用listen()(之前,服务器端的套接字处于主动状态,它只能用于发起连接请求(比如客户端连接其他服务器)
-
- 调用listen()后,套接字被转换为被动套接字,即它不会发起连接,而是等待客户端连接请求。
- (2)维护连接请求队列:
-
- 内核为套接字维护两个队列:未完成队列和已完成队列
-
- 未完成队列:存储那些已发起连接但尚未完成三次握手的客户端请求。
-
- 已完成队列:存储那些已经完成三次握手等待accept()的客户端连接。
- backlog参数决定了这两个队列的总长度上线,如果队列已满,新的连接请求将会被拒绝。
backlog并不是严格定义允许的最大连接数,而是一个指导值,操作系统可能会根据实际情况调整该值。如果backlog设置的过小,服务器可能会丢失一些连接请求,特别是在高并发场景下。不同操作系统对backlog的处理方式略有不同,但一般建议根据应用场景 设置合适的值。
- 常见错误以及解决方案
- EBADF:sockfd参数不是有效的文件描述符。通常是由于传递了未创建的套接字描述符
- ENOTSOCK:文件描述符不是一个套接字,可能误用了其他类型的文件描述符
- EADDRINUSE:绑定的地址已被另一个套接字使用,可以使用setsockopt()设置SO_REUSEADDR选项来允许地址重用。
- EINVAL:sockfd套接字未通过bind()函数绑定到一个本地地址或未设置为TCP类型套接字(SOCK_STREAM)
- EOPNOTSUPP:使用了不支持listen()操作的套接字类型,比如UDP套接字。
- listen()与TCP三次握手的关系
- 当服务器调用listen()(后,它开始等待客户端的连接请求。客户端发起连接请求时,会进行TCP的三次握手过程:a.客户端发送SYN包给服务器,表示请求建立连接。b.服务器返沪SYN-ACK包,表示同意建立连接。c.客户端返回ACK包,连接建立完成。
- 在三次握手完成之前,连接请求会被放入未完成队列;三次握手完成后,连接请求会被移到已完成队列。
- 服务器可以通过accept()函数获取已完成连接队列中的客户端连接。
6.accept()函数
accept()函数是服务器端套接字编程中的关键函数,用于从连接队列中取出等待的客户端连接,并为其创建一个新的套接字。通过accept(),服务器可以与客户端进行后续通信。
- 函数原型
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
- sockfd:服务器监听套接字的文件描述符,表示等待连接的套接字
- addr:指向sockaddr结构体的指针,用于存储客户端的地址信息
- addrlen:指向socklen_t类型变量的指针,传递给accept()用于表示addr的长度,并在函数返回时保存客户端地址的长度。
2.返回值
(1)成功时,accept()返回一个新的套接字描述符(new_sockfd),该套接字专用于与该客户端的通信。
(2)失败时,accept(()返回-1.并设置errno以指示错误类型,如: - EAGAIN或EWOULDBLOCK:套接字为非阻塞模式,但没有连接请求处于等待状态。
- EBADF:无效的文件描述符
- ECONNABORTED:客户端的连接请求被中止
- EINTR:函数调用被中断
- EINVAL:监听套接字无效,可能尚未调用listen()
- ENOTSOCK:sockfd不是一个套接字
·················
- 阻塞与非阻塞模式
- 阻塞模式:在默认情况下,accept()是阻塞的,如果没有客户端连接,accept()会一直等待,直到有客户端连接进来,;如果由客户端连接进来,accept()立即返回,返回值是与该客户端通信的新的套接字描述符。
- 非阻塞模式:如果将套接字设置为非阻塞模式,accpt()调用立即返回,如果没有等待连接,accept()返回-1,并设置errno为EAGAIN或EWOULDBLOCK。可以通过使用fcntl()函数将套接字设置为非阻塞模式。
fcntl(sockfd,F_SETFL,O_NONBLOCK);
- accept()阻塞的处理过程
当服务器调用accept()后,如果当前没有客户端连接请求,accept()会进入阻塞状态,此时服务器会处于等待状态,直到有新的客户端尝试连接。在阻塞期间,服务器的主线程无法执行其他操作,必须等待accept()返回新的连接。
·当有客户端连接时:
-
- accept()解除阻塞,取出连接队列中的第一个连接请求。返回用于与该客户端通信的新的套接字描述符。
如果连接队列中没有等待的连接请求,accept()会继续阻塞,直到有新的连接到来。
- accept()解除阻塞,取出连接队列中的第一个连接请求。返回用于与该客户端通信的新的套接字描述符。
accept()函数本身不自带无限循环,它只负责从服务器的连接队列中取出一个客户端连接并返回该连接的套接字。因此,accept()每次只处理一个客户端连接,当该连接被接受之后,服务器需要再次调用accept()来等待新的客户端连接。
因此,在实际的服务器编程中,为了能够不断接受来自不同客户端的连接,通常会在accept()的外部包裹一个while(true)或类似的循环。这是因为服务器需要不断地监听新的连接,而不仅仅是处理一次连接请求。
7.recv()函数
- 函数定义
ssize_t recv(int sockfd,void *buf,size_t len,int flags);
返回值:表示实际接收收到的数据字节数,失败时返回-1,并设置errno指示错误。
···
2. 参数详解
-
sockfd:套接字描述符,指出你想要接收数据的套接字
-
buf:类型是:void *;其作用:这是一个缓冲区指针,指向用于存储收到的数据的内存区域。该内存区域由调用者分配,recv()函数将接收到的数据复制到这个缓冲区中。
-
len:表示可以接受的最大字节数,即缓冲区buf的大小,recv()将最多接收len字节的数据,存储在buf中
-
flags:这个参数是提供额外的控制选项,影响recv()的行为,常见的选项包括:
-
- 0:默认模式,没有特殊标志
-
- MSG_PEEK:从缓冲区中窥视数据,不移除它,这意味着数据仍然保留在套接字接收队列中,下次调用recv()时仍然可以读取到相同的数据。
-
- MSG_WAITALL:等待接收完整的len字节,直到所有数据被接收或连接关闭
-
- MSG_OOB:接收带外数据,通常用于TCP的紧急数据
- 返回值
- '> 0表示成功接收的数据字节数
- 0:表示连接已被对方关闭(对于TCP连接)
-1:表示出现了错误,并设置errno指示错误原因
特别注意
1.阻塞行为:默认情况下,recv()是阻塞的。也就是说,当没有数据可以接受时,recv()会阻塞,直到有数据可用或连接关闭,如果你希望让recv()在没有数据时立即返回而不是阻塞,可以将套接字设置为非阻塞模式,或者使用MSG_DONTWAIT标志
2.连接关闭:当recv()返回0时,表示对方关闭了连接。在TCP连接中,这是一个常见的方式来检测客户端是否断开
3.数据长度问题:recv(并不能保证每次接收的数据量等于你请求的长度len。数据可能会分多次传送,尤其是在网络不稳定的情况下。因此,如果你需要接收固定大小的数据,你需要检查recv()的返回值,并在必要时多次调用recv()以获取完整数据。
8.send()函数
send()函数用于通过已连接的套接字发送数据,通常在TCP网络编程中使用,它与recv()时对应的操作,recv()负责接收数据,而send()负责发送数据。
- 函数定义
sszie_t send(int sockfd,const void* buf,size_t len,int falgs);
返回值:表示成功发送的字节数,如果出现错误则返回-1,并设置errno来指示错误。
- 参数详解
-
sockfd:套接字描述符
-
buf:指向要发送的数据的缓冲区,即存放要通过网络传输的数据。发送的数据会从该缓冲区中读取。
-
len:指定要发送的字节数,也就是缓冲区中数据的长度。send()函数会尝试发送len个字节的数据,但不保证一次就嫩那个发送全部数据。
-
flags:控制发送行为的标志,常用的标志包括:
-
- 0:默认标志,没有特殊选项。
-
- MSG_DONTWAIT:非阻塞发送,立即返回而不等待缓冲区可用。如果缓冲区已满,函数会返回-1,并设置errno为EAGAIN或EWOULDBLOCK
-
- MSG_OOB:发送带外数据(紧急数据)。在TCP中,这种数据通常用于传输控制信息。
-
- MSG_NOSIGNAL:阻止在连接被关闭时发送SIGPIPE信号,防止进程异常终止。
- 返回值
- ·>0:表示成功发送的字节数,可能小于len,这意味着并未将全部数据一次性发送完
- 0:在send()中返回0并不常见,通常不会用于指示成功的传输结束
- -1:表示出现了错误,errno会设置为具体的错误码。
- 发送数据长度问题
在网络编程中,特别是对于send()函数,不能保证依次调用send()会将所有数据发送完。特别是在传输大块数据时,send()可能只发送了一部分数据,然后返回实际发送的字节数。这时需要通过循环调用send()来确保所有数据都已发送。
以下是一个例子
ssize_t total=0;
ssize_t bytes_sent;
const char *data="A long message";
size_t data_len=strlen(data);
//循环发送直到所有数据发送完毕
while(total<data_len)
{butes_sent=sned(sock,data+total,data_llen-total,0);if(bytes_sent==-1){std::cerr<<"Send Error"<<std::endl;break;}total+=butes_sent;
}
- 特别注意
- 阻塞行为:默认情况下,send()是阻塞的,也就是说,如果系统的发送缓冲区已满,send()会等待直到有足够空间可以发送数据。在这种情况下,程序会暂停执行,直到数据成功发送或出现错误。
- 非阻塞模式:如果套接字被设置为非阻塞模式,或者send()函数使用了MSG_DONTWAIT标志,那么当发送缓冲区时,send()会立即返回,可能返回-1,并将errno设置为EAGAIN或EWOULDBLOCK
- 数据丢失:send()函数负责将数据交给操作系统内核,实际的数据传输过程由内核管理,如果网络连接出现问题,数据可能无法送达目的地,但send()并不会返回传输失败,除非连接已经断开(例如,ECONNRESET或EPIPE错误)。
- send()用于将数据从应用层发送到网络上,通过套接字通信
- 函数的flags参数允许你控制数据发送的行为,例如发送带外数据(紧急数据)、非阻塞发送等。
- send()是阻塞的,除非使用了非阻塞模式或相应的flags
- 数据发送时,send()不保证一次发送全部数据,可能需要多次调用才能传输完整数据。
9.close()函数
close()函数用于关闭套接字,当一个应用程序不再需要与远程注意进行通信时,调用此函数可用释放相关的资源。套接字的关闭过程涉及到网络连接的中止和资源的清理。在网络编程中,特别是设计TCP协议时,close()的行为比较复杂,因为它需要处理连接的安全中止,确保所有数据都成功传输。
- 函数定义
int close(int sockfd);
返回值:成功返回0,失败返回-1,并设置errno指示错误。
-
close()的作用
*释放资源:当调用close()时,系统会回收该套接字占用的资源,包括网络缓冲区、套接字描述符等,否则会导致资源泄露,最终可能耗尽系统资源。
通知对端:对于基于TCP的连接,close()函数会通知对端,本地应用程序已经关闭连接,不会再发送数据。TCP会通过四次挥手(Four-way handshake)来确保连接的正常终止 -
关于TCP协议中的close()
在TCP连接中,close()不仅仅是简单的关闭文件描述符,它还会触发一些列与TCP协议相关的步骤:
- 发送FIN:当应用程序调用close()关闭TCP连接时,操作系统会向对端发送一个FIN(finish)报文,表示本地不会再发送数据。
- 等待对端确认:对端接收到FIN报文后,会回复一个ACK确认包,表示它已经知道本地关闭了连接。
- 等待对端的FIN:对端接收到FIN报文后,会回复一个ACK确认包,表示它已经知道本地关闭了连接。
- 接收ACK:本地再回复一个ACK ,表示已经收到对端的关闭请求,至此连接正式断开。
这种过程称为TCP的四次挥手,在调用close()后,连接不会立即断开而是需要等待对端的确认。这个过程会让套接字进入TIME_WAIT状态,通常持续2分钟,以确保网络上的延迟不会影响连接的安全关闭。
- close()的行为
- 关闭发送/接收通道:close()会同时关闭TCP连接的发送和接收通道。这意味着本地程序无法再通过该套接字发送或接收任何数据
- 数据未发送完:如果再调用close()时,发送缓冲区中仍有数据未穿输完,操作系统会试图讲过这些数据继续发送,但如果需要立即中断数据传输,可使用shutdown()函数,它提供了更精细的控制。
- 阻塞与非阻塞:调用close()后,程序不会等待远程主机的响应,而是立即返回,并把关闭操作符交给操作系统处理。这意味着close()是非阻塞操作。
- close()与shutdown()的区别
- close()关闭整个套接字,包括发送和接收的功能,并释放资源。
- shutdown()提供了更精细的控制,可以选择仅关闭发送或接收的通道,而不关闭整个套接字。
- shutdown()的函数签名如下:
int shutdown(int sockfd,int how);
how参数:控制关闭行为,取值如下:
-
- SHUT_RD:关闭读通道,程序不能再从套接字接收数据
-
- SHUT_WR:关闭写通道,程序不能在向套接字发送数据
-
- SHUT_RDWR:同时关闭读写通道,相当于close()
- 注意事项
- 多次close():同一个套接字描述符只能关闭一次,调用close()后,该套接字描述符变得无效,如果再次调用close()会返回-1,并设置errno为EBADF。
- TIME_WAIT状态:在TCP连接中,调用close()后,套接字可能会进入TIME_WAIT状态,防止旧数据影响新连接,通常该状态会持续2分钟。
- 文件描述符复用:在调用close()后,文件描述符可能被操作系统重新分配给其他文件或套接字,因此不要再对已经关闭的文件描述符进行任何操作。
七、TCP客户端的实现
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
#include <arpa/inet.h>using namespace std;int main()
{int sock;struct sockaddr_in server;//创建socketsock=socket(AF_INET,SOCK_STREAM,0);if(sock<0){cerr<<"Could not create socket"<<endl;return 1;}//设置服务器地址server.sin_family=AF_INET;server.sin_port=htons(8888);server.sin_addr.s_addr=inet_addr("127.0.0.1");//连接服务器if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0){cerr<<"Could not connect to server"<<endl;return 1;}//发送数据const char* message="Hello from client";send(sock,message,strlen(message),0);//接收数据char buf[1024];recv(sock,buf,sizeof(buf),0);cout<<"Server response: "<<buf<<endl;//关闭socketclose(sock);return 0;
}