一、套接字描述符
1、创建一个套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
//返回值:成功返回文件描述符,失败返回-1
- domain:
指定通信地址族
,通常以AF_开头(Address Family)- AF_INET:IPv4
- AF_INET6:IPv6
- AF_UNIX:UNIX域
- AF_PACKET:原始套接字
- type:
确定套接字类型
- SOCK_STREAM:TCP
- SOCK_DGRAM:UDP
- SOCK_RAW:原始套接字
- protocol:
指定协议类型
,通常为0,表示使用默认协议,- TCP:IPPROTO_TCP
- UDP:IPPROTO_UDP
- ICMP:IPPROTO_ICMP
- IGMP:IPPROTO_IGMP
- IPV4:IPPROTO_IP
- IPV6:IPPROTO_IPV6
2、禁止一个套接字
#include <sys/socket.h>
int shutdown(int sockfd, int how);
//返回值:成功返回0,失败返回-1
- sockfd:
套接字描述符
- how:
指定关闭方式
- SHUT_RD:关闭读端
- SHUT_WR:关闭写端
- SHUT_RDWR:关闭读写端
3、关闭一个套接字
#include<unistd.h>
int close(int fd);
//返回值:成功返回0,失败返回-1
- fd:
套接字描述符
二、寻址
1、字节序转换函数
有些处理器使用小端字节序,有些处理器使用大端字节序。但是网络协议可能使用的字节序与处理器不同(如TCP/IP使用大端字节序),因此需要将字节序和网络字节序之间进行转换。
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
- htonl:
将主机字节序转换为网络字节序
- htons:
将主机字节序转换为网络字节序
- ntohl:
将网络字节序转换为主机字节序
- ntohs:
将网络字节序转换为主机字节序
h表示主机字节序,n表示网络字节序,l表示长整型,s表示短整型。
2、地址格式
地址格式与通信域相关。为了使不同的格式地址都能传入套接字函数,地址会被强制转换成一个通用的结构体sockaddr。
struct sockaddr
{sa_family_t sa_family; /* address family, AF_xxx */char sa_data[];
};
- sa_family:
地址族
- sa_data:
协议地址
套接字可以自由的添加额外的成员并定义sa_data的大小。
a、ipv4格式
在IPV4因特网域(AF_INET)中,sockaddr_in结构体被定义如下:
struct in_addr
{uint32_t s_addr; //IPV4地址
};
struct sockaddr_in
{sa_family_t sin_family; /* 地址族: AF_INET */in_port_t sin_port; /* 端口号,网络字节序 */struct in_addr sin_addr; /* IP地址,网络字节序 */
}
in_port_t
被定义为uint16_t
in_addr_t
被定义为uint32_t
b、ipv6格式
IPv6因特网域(AF_INET6)中,sockaddr_in6结构体被定义如下:
struct in6_addr
{uint8_t s6_addr[16]; //IPV6地址
};
struct sockaddr_in6
{sa_family_t sin6_family; /* 地址族: AF_INET6 */in_port_t sin6_port; /* 端口号,网络字节序 */uint32_t sin6_flowinfo; /* IPv6流信息 */struct in6_addr sin6_addr; /* IPv6地址,网络字节序 */uint32_t sin6_scope_id; /* IPv6作用域ID */
};
c、Linux中ipv4格式
在Linux中,sockaddr_in结构体被定义为:
struct sockaddr_in
{sa_family_t sin_family; /* 地址族: AF_INET */in_port_t sin_port; /* 端口号,网络字节序 */struct in_addr sin_addr; /* IP地址,网络字节序 */unsigned char sin_zero[8]; /* 8个字节,填充0 */
}
注意:尽管sockadd_in和sockaddr_in6结构差距比较大,但是它们都被强制转换成sockaddr结构体,因此可以传入套接字函数。
3、地址转换函数
注意:这两个函数仅支持IPv4地址
。
-
inet_addr
函数将点分十进制IP地址转换为二进制IP地址。 -
iner_ntoa
函数将二进制IP地址转换为点分十进制IP地址。
#include <arpa/inet.h>
const char *inet_ntoa(struct in_addr in);
//返回值:成功返回指向点分十进制IP地址的指针,失败返回NULLin_addr_t inet_addr(const char *cp);
//返回值:成功返回指向in_addr结构的指针,失败返回INADDR_NONE
注意:下面这两个函数同时支持IPv4和IPv6地址
。
-
inet_pton
函数将点分十进制IP地址转换为二进制IP地址。 -
inet_ntop
函数将二进制IP地址转换为点分十进制IP地址。
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *restrict addr, char *restrict str, socklen_t size);
//返回值:成功返回指向str的指针,失败返回NULLint inet_pton(int af, const char *restrict str, void *restrict addr);
//返回值:成功返回1,失败返回0
- af:
地址族
- src:
指向点分十进制IP地址的指针
- addr:
指向二进制IP地址的指针
4、INADDR_ANY
INADDR_ANY:表示任意地址。
- 它表示服务器将接受来自任何网络接口的请求
- 当服务器需要监听多个网络接口时,可以使用INADDR_ANY来绑定所有网络接口,而不需要指定具体的IP地址。
#define INADDR_ANY ((in_addr_t) 0x00000000)
使用方法:
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
//此处可以不进行将主机字节序转换成网络字节序,因为是0
//address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(1234);
5、地址查询
a、getaddrinfo函数
用于获取主机信息。
#include <netdb.h>
struct hostent *gethostent(void);
//返回值:成功返回指向hostent结构的指针,失败返回NULLvoid sethostent(int stayopen);
void endhostent(void);
hostent结构体定义如下:
struct hostent
{char *h_name; /* 主机名 */char **h_aliases; /* 主机别名列表 */int h_addrtype; /* 地址类型 */int h_length; /* 地址长度 */char **h_addr_list; /* IP地址列表 */...
}
b、gethostbyname(已过时)
通过主机名(如www.example.com)获取对应的IPv4地址。
struct hostent *gethostbyname(const char *name);
//返回值:成功返回指向hostent结构的指针,
// 失败返回NULL,可通过h_errno变量获取错误码。
- name:
主机名
- addr:
IP地址
- len:
IP地址长度
- type:
地址类型
注意:
- 仅支持IPv4。
- 非线程安全(不可重入)。
已逐渐被更现代的getaddrinfo替代
。
c、gethostbyaddr(已过时)
通过IPv4地址获取对应的主机名(反向DNS查询)。
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
//返回值:成功返回指向hostent结构的指针,
// 失败返回NULL,可通过h_errno变量获取错误码。
-
addr:指向IPv4地址的指针(需转换为struct in_addr格式)。
-
len:地址长度(IPv4为4)。
-
type:地址类型(AF_INET表示IPv4)。
-
现在由getnameinfo函数替代
。
d、getnameinfo(推荐)
将一个地址转换成 一个主机名和服务名。
#include <netdb.h>
#include <sys/socket.h>int getnameinfo(const struct sockaddr *restrict addr, socklen_t addrlen,char *restrict host, socklen_t hostlen,char *restrict serv, socklen_t servlen, int flags);
//返回值:成功返回0,失败返回错误码
- addr:指向sockaddr结构的指针。
- addrlen:sockaddr结构的长度。
- host:指向主机名的指针。
- hostlen:主机名缓冲区的大小。
- serv:指向服务名的指针。
- servlen:服务名缓冲区的大小。
- flags:控制函数行为的标志。
host非空
,则指向一个长度为hostlen的缓冲区用于存放返回的主机名
。
serv非空
,则指向一个长度为servlen的缓冲区用于存放返回的服务名
。
flags参数可以控制函数的行为,例如:
- NI_DGRAM:指定协议类型为UDP,而不是TCP。
- NI_NOFQDN:不返回完整的域名,只返回主机名。
- NI_NUMERICHOST:返回数字形式的IP地址,而不是主机名。
- NI_NAMEREQD:如果无法解析主机名,则返回错误。
- NI_NUMERICSERV:返回数字形式的端口号,而不是服务名。
f、getaddrinfo(推荐)
将主机名和服务名转换成套接字地址。
#include <netdb.h>
#include <sys/socket.h>
int getaddrinfo(const char *rest host, const char *restrict serv,const struct addrinfo *restrict hints,struct addrinfo **restrict res);
//返回值:成功返回0,失败返回错误码void freeaddrinfo(struct addrinfo *ai);
//释放addrinfo结构体
- host:主机名或IP地址。
- serv:服务名或端口号。
- hints:指向addrinfo结构的指针,用于指定查询的约束条件。
- res:指向addrinfo结构体的指针,用于存放查询结果。
//返回值:成功返回0,失败返回错误码
hints
选择符合特定条件的地址信息,例如:
- AI_ADDRESS_FAMILY:指定地址族(AF_INET或AF_INET6)。
- AI_ALL:查找IPV4和IPV6地址。(仅用于AI_V4MAPPED)
- AI_CANONNAME:返回规范主机名。
- AI_NUMERICHOST:仅返回数字形式的IP地址。
- AI_NUMERICSERV:仅返回数字形式的端口号。
- AI_PASSIVE:用于服务器端,返回通配符地址(INADDR_ANY)。
- AI_V4MAPPED:如果找不到IPv6地址,返回映射到IPV6格式的IPV4地址。
注意:
-
需要提供
主机名
和服务名
,或者两个都提供,如果只提供一个
,另外一个必须是空指针
。
主机名可以是一个域名,也可以是一个IP地址。 -
getaddrinfo返回一个
addrinfo结构体
的链表,可以使用freeaddrinfo
函数释放这个链表,其中可以包含一个或多个addrinfo结构
,用于表示不同的地址。
6、将套接字和地址关联
bind函数将套接字与地址关联起来。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t len);
//返回值:成功返回0,失败返回 -1
注意:
- 指定的地址必须有效,不能指定一个其他机器上的地址。
- 地址必须与地址组所支持的格式匹配。
- 地址中端口号不能小于1024,除非进程具有超级用户权限。
- 一般只能将一个套接字端点与一个地址关联。
如果指定IP地址为INADDR_ANY,则表示服务器将接受来自任何网络接口的请求。
使用getsockname来发现绑定到套接字上的地址。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict alenp);
//返回值:成功返回0,失败返回 -1
- addr:指向sockaddr结构的指针,用于存放返回的地址。
- alenp:指向socklen_t类型的指针,用于存放返回的地址长度。
三、建立连接
1、connect函数
客户端使用connect函数与服务器建立连接。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
//返回值:成功返回0,失败返回 -1
- sockfd:套接字描述符。
- addr:指向sockaddr结构的指针,用于指定服务器的地址。
如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址。
连接服务器可能会出现失败,服务器必须是开启的,并且正在运行,并且服务器的等待连接队列要有足够的空间。因此,应用程序需要处理connect返回的错误。
//可能出现错误的connect
for(numsec = 1; numsec <= MAXSLEEP; numsec <<= 1)
{if(connect(fd, servaddr, servlen) == 0){return 0;}if(numsec <= MAXSLEEP / 2){sleep(numsec);}
}
注意:使用指数退避算法,在连接失败后会休眠一段时间,然后重试。每次重试的间隔时间以指数级增加,直到达到最大延迟(通常为2分钟)。这种方法在Linux和Solaris上有效,但在FreeBSD和Mac OS X上存在问题,因为基于BSD
的系统在首次连接失败后,继续使用同一个套接字描述符会失败
。
我们可以每次都创建一个新的套接字
描述符来连接服务器,这样就可以避免这个问题。
for(numsec = 1; numsec <= MAXSLEEP; numsec <<= 1)
{if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 1){return -1;}if(connect(fd, servaddr, servlen) == 0){return 0;}close(fd);if(numsec <= MAXSLEEP / 2){sleep(numsec);}
}
2、listen函数
服务器使用listen函数宣告它愿意接受连接请求。
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//返回值:成功返回0,失败返回 -1
-
sockfd:套接字描述符。
-
backlog
:等待连接队列的最大长度
。
参数backlog指定了等待连接队列的最大长度,即服务器最多可以有多少个连接请求在等待被处理。
如果等待连接队列已满,新的连接请求将被拒绝。其
实际长度
由系统决定,但是上限由<sys/socket.h> 中的SOMAXCONN
指定。Solaris系统中,会忽视SOMAXCONN,具体的最大值取决于每个协议的实现。对于TCP,最大值通常为128。
3、accept函数
一旦服务器调用了listen,所用的套接字就能接收到连接请求。使用accept
函数获得连接请求并建立连接。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict len);//返回值:成功返回新的套接字描述符,失败返回 -1
accept返回的是新的套接字描述符,这个新套接字描述符
和原始套接字描述符(sockfd)具有相同的类型和地址族。
原始套接字描述符(sockfd)
没有关联到这个连接,而是继续保持可用状态并接受连接
。
如果服务器调用accept,并且当前没有连接请求,那么服务器将阻塞,直到有连接请求到达。服务器可以使用poll或select
来等待一个请求的到来,在这种情况下,一个带有等待连接的套接字描述符将变为可读
。
四、数据传输
1、发送数据
a、send函数
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
//返回值:成功返回发送的字节数,失败返回 -1
- sockfd:套接字描述符。
- buf:指向缓冲区的指针,用于存放接收到的数据或发送的数据。
- len:缓冲区的大小。
- flags:控制函数行为的标志。
send与write类似,使用send时套接字必须已经连接。参数buf和len与write中一致。
不同的是,send可以指定一些额外的标志(flags)
flags
:
- MSG_CONFIRM:提供链路层反馈以保持地址映射有效。
- MSG_DONTROUTE:勿将数据包路由出本地网络。
- MSG_DONTWAIT:允许非阻塞操作。
- MSG_EOF:发送数据后关闭套接字发送端。
- MSG_EOR:如果协议支持,标记记录结束。
即使send成功返回
,也不代表连接的另一端接收到了数据。但是此时数据已经被无错误的发送到了网络驱动程序
上。
b、sendto函数
#include <sys/socket.h>
sisize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *destaddr, socklen_t destlen);
//返回值:成功返回发送的字节数,失败返回 -1
- sockfd:套接字描述符。
- buf:指向缓冲区的指针,用于存放接收到的数据或发送的数据。
- len:缓冲区的大小。
- flags:控制函数行为的标志。
- destaddr:指向sockaddr结构的指针,用于指定目标地址。
- destlen:目标地址的长度。
sendto函数与send函数类似,但是sendto函数可以用于无连接的套接字
,例如UDP套接字。sendto函数需要指定目标地址
,而send函数不需要。
c、sendmsg函数
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
//返回值:成功返回发送的字节数,失败返回 -1
- sockfd:套接字描述符。
- msg:指向msghdr结构的指针,用于指定要发送的数据。
- flags:控制函数行为的标志。
sendmsg函数可以一次发送多个缓冲区
。sendmsg函数的参数msg是一个msghdr结构,它包含了要发送的数据和目标地址。
msghdr结构
的定义如下:
struct msghdr {void *msg_name; /* 指向sockaddr结构的指针,用于指定目标地址 */socklen_t msg_namelen; /* 目标地址的长度 */struct iovec *msg_iov; /* 指向iovec结构的指针,用于指定要发送的数据 */int msg_iovlen; /* iovec结构的数量 */void *msg_control; /* 指向cmsghdr结构的指针,用于指定控制信息 */socklen_t msg_controllen; /* 控制信息的长度 */int msg_flags; /* 控制标志 */
};
- msg_name:指向sockaddr结构的指针,用于指定目标地址。
- msg_namelen:目标地址的长度。
- msg_iov:指向iovec结构的指针,用于指定要发送的数据。
- msg_iovlen:iovec结构的数量。
2、接收数据
a、recv函数
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
//返回值:成功返回接收到的字节数,失败返回 -1
- sockfd:套接字描述符。
- buf:指向缓冲区的指针,用于存放接收到的数据。
- len:缓冲区的大小。
flags
:控制函数行为的标志。
-
MSG_CMSG_CLOEXEC:为UNIX域套接字接受的文件描述符设置关闭标志。
-
MSG_DONTWAIT:允许非阻塞操作。
-
MSG_ERRQUEUE:从套接字的错误队列中接收数据。
-
MSG_OOB:如果协议同意,接收带外数据。
-
MSG_PEEK:返回数据包内容而不真正取走数据包。
-
MSG_TRUNC:返回数据包的实际长度,即使它比缓冲区大。
-
MSG_WAITALL:等待所有数据到达。
当指定
MSG_PEEK
标志时,可以查看下一个要读取的数据但不真正取走它。当再次调用read
或者其中一个recv
函数时,会返回刚才查看的数据
。对于
SOCK_STREAM套接字
,接受的数据可能比预期的少,MSG_WAITALL
标志可以用来等待所有数据
到达。
b、recvfrom函数
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags,struct sockaddr *restrict addr, socklen_t *restrict addrlen);
//返回值:成功: 返回接收到的字节数,
// 若无可用数据或连接已关闭: 返回0,
// 失败: 返回 -1
- sockfd:套接字描述符。
- buf:指向缓冲区的指针,用于存放接收到的数据。
- len:缓冲区的大小。
- flags:控制函数行为的标志。
如果addr非空
,它将包含数据发送者
的套接字端点地址
。
因为可以获得发送者的地址,recvform通常被用于无连接的套接字
。否则recvfrom == recv。
c、recvmsg函数
recvmsg函数可以 一次接收多个缓冲区
,还可以接收辅助信息
。
#include <sys/socket.h>ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//返回值:成功: 返回接收到的字节数,
// 若无可用数据或连接已关闭: 返回0,
// 失败: 返回 -1
- sockfd:套接字描述符。
- msg:指向msghdr结构的指针,用于指定要接收的数据。
- flags:控制函数行为的标志。
常见的flags有:
- MSG_CTRUNC:如果控制信息太长,则截断。
- MSG_EOR:如果协议支持,标记记录结束。
- MSG_ERRQUEUE:从套接字的错误队列中接收数据作为辅助信息。
- MSG_OOB:接收带外数据。
- MSG_TRUNC:一般数据被阶段。