5-1. C++ Socket基本函数库介绍
本文介绍了Linux C++的socket api一系列方法,以及如何使用Linux C++的socket api搭建较为简易的服务器与客户端;并在5-2篇文章中将socket api的方法与tcp连接的状态进行深入剖析;最后在5-3章中继续深入剖析网络编程的技巧、方法与模型。
1. Socket
1.1 Socket基本概念
Socket,又叫套接字,是网络编程中的一个抽象概念,可简单的理解为socket标识一个网络连接。
互联网中的两个进程想要建立TCP/UDP连接,需要双方的ip地址+端口号,网络协议栈中由网络层提供的ip地址用于标识主机(网卡),传输层协议提供的端口号用于标识进程;有了双方的ip+port+协议类型便可以唯一确定一个网络连接。
因此,Socket由五元组唯一确定 {protocol,src_addr,src_port,dest_addr,dest_port},其中protocol指定了是TCP还是UDP连接,其余参数分别指定了源地址、源端口、目标地址、目标端口。
如下图所示:Socket是在应用层和传输层之间的一个抽象层,它把传输层和网络层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。
1.2 Socket与文件描述符
众所周知,在Unix和Linux操作系统中具有一切皆文件的思想。在Linux系统中建立一个Socket网络连接,可以抽象理解为系统内核空间的文件进行“打开-读写-关闭”动作,即客户端建立一个Socket相当于声明一个文件指针;连接服务器相当于fopen文件;向服务器发送数据相当于fwrite文件;接收服务端数据相当于fread文件;关闭连接相当于fclose文件;因此,这片独有的内核空间可被抽象理解为文件。
总结:Socket能够唯一标识一个网络连接,可将其抽象为文件指针,指向内核空间(一个文件)。Socket等价于内核的一个文件描述符,客户端与服务器建立TCP连接后,双方各持有一个socket指向内核空间;客户端向服务器发送数据时,数据由客户端网络协议栈层层包装为字节流后发送到服务器的内核中,服务器进程通过socket指向的内核空间获取该数据。
2. C++ Socket API
2.1 socket()函数
Linux C++中的socket API包含在 sys/types.h 和 sys/socket.h这两个头文件中,socket()函数就是创建网络通信的标识并返回一个文件描述符。
int socket(int domain, int type, int protocol) ;
- 第一个参数domain是网络连接协议簇的标识,通常为AF_INET和AF_INET6,即ipv4和ipv6的网络连接。协议族决定了socket的地址类型,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
- 第二个参数type是网络通信的语义类型,对于TCP连接通常选择SOCK_STREAM,字节流语义。
- 第三个参数是与语义类型相匹配的特定协议,TCP连接使用IPPROTO_TCP,UDP连接使用IPPTOTO_UDP。 注意:type和protocol不是可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。 -返回值:系统分配的套接字id。
2.2 bind()函数
通过socket()函数已经获得了一个套接字,我们指定了这个套接字的协议簇,语义类型和协议,但还未赋予其具体的地址。因此,需要调用bind()函数将一个特定地址绑定到socket上。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd: socket套接字,通过socket()创建得到。
- addr: 将sockfd绑定的协议地址,一个const struct sockaddr指针。该参数与创建socket时的协议簇相关联: -1. AF_INET即ipv4时:int变量(4Byte)能表示一个ip地址
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
-2.AF_INET6即ipv6时:16length字符串表示ip地址
struct sockaddr_in6 {
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port; /* port number */
uint32_t sin6_flowinfo; /* IPv6 flow information */
struct in6_addr sin6_addr; /* IPv6 address */
uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};
struct in6_addr {
unsigned char s6_addr[16]; /* IPv6 address */
};
-addrlen:地址结构体的长度
2.3 listen()函数
当在第二步完成对socket的地址绑定后,服务端需要调用listen()来监听socket,处于监听状态下的socket便可以接受客户端的连接、断开请求。
如listen函数的帮助手册所示:
int listen(int sockfd, int backlog);
- sockfd: socket套接字,通过socket()和bind()得到。
- backlog: socket的未完成连接队列长度,在后续的文章中详细讨论。
- 返回值: 监听成功返回0,否则返回小于0。
2.4 connect()函数
connect()函数顾名思义,由主动socket向被动socket发起连接请求,因此connect函数中的socket被称为主动socket,同理listen函数处理后的socket为被动socket。
如connect的帮助手册所示:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd: 主动socket,即客户端向服务器通信的socket;注意不是被动socket。
- addr: 与bind()函数的地址一致,为服务端socket的地址。
- addrlen: 地址长度。
- 返回值: 连接成功返回0,否则返回值小于0。
注意:man手册对connect()函数返回值的描述,If the connection or binding succeeds, zero is returned. 为什么bind()会出现在connect返回值描述中呢?因为对于客户端来说,它的socket不需要主动进行bind()调用去绑定一个端口,在connect()调用时候系统会隐形执行bind(),绑定的地址为:系统分配的端口和本机的ip。
2.5 accept()函数
accept()函数是用于面向基于连接的套接字类型(SOCK_STREAM,SOCK_SEQPACKET)使用,它为处于监听状态的sockfd提取其未连接队列上的第一个连接请求,使其成为一个新的已连接套接字,并返回为其新创建的文件描述符。
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd: listen状态的被动套接字;
- addr: 连接到listen sockfd的客户端地址指针;
- addrlen: 地址长度
- 返回值:成功建立连接返回大于0的正整数代表内核创建的sockfd,异常时返回-1;
关于connect、accept函数与tcp连接状态的关系,在5-2章中做详细讲述。
2.6 recv()函数
recv()函数用于从面向连接的套接字中获取消息,此外recvfrom()和recvmsg()无论是否面向连接,都可以用来获取消息。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd: accept()建立连接后返回用于通信的sockfd;
- buf: 接受消息存放的消息结构;
- len: 消息结构的大小;
- flags: 一系列消息接收特性的或运算,例如:MSG_DONTWAIT 非阻塞recv等,默认可传0;
- 返回值:返回接收到消息字节长度,如果异常产生返回值小于0;
- 一些常见的错误包括:EAGAIN 一般为非阻塞socket出现接收阻塞或超时;EINTR 通常为接收操作被信号中断。
2.7 send()函数
send()函数用于发送消息到sockfd,且sockfd为为已连接状态;
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- sockfd: 建立连接后分配的socket的文件描述符;
- buf:发送数据的具体内容;
- len: 发送数据的长度;
- flags: 0或多个标志位或运算得到的特性,例如:MSG_DONTWAIT 或者 MSG_NOSIGNAL 当对方socket断开连接时,在面向字节流的套接字上针对错误不发送SIGPIPE。
- 返回值:成功时 返回发送的字节数;异常时 返回值小于0
2.8 close()函数
close()用于关闭一个文件描述符,不仅仅可用于网络套接字的关闭,也可以用于普通文件读写。
注意:帮助手册的最后一段话:如果文件描述符的引用最后一次被关闭,那么就会释放文件相关的资源。这里会涉及网络编程中 服务器与客户端优雅断开连接的问题,在后续的文章中进行详细讨论。
- fd: 被关闭的文件描述符
- 返回值:正常返回0,异常情况下返回值小于0,例如:EBADF 无效的fd, EINTR close()被信号中断;
2.9 fcntl()函数
函数原型:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
阻塞和非阻塞的区别在于调用某个函数时,这个函数是否会导致
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> C++工程师面试真题解析! </p> <p> 邀请头部大厂创作者<a href="https://www.nowcoder.com/profile/73627192" target="_blank">@Evila</a> 及牛客教研共同打磨 </p> <p> 助力程序员的求职! </p>