Linux 系统编程中,网络套接字(socket)是一个重要的概念。套接字是一种在网络上进行通信的工具,通过套接字可以建立客户端和服务器之间的连接,并进行数据的传输。
在 Linux 系统中,网络套接字是一种特殊的文件描述符,可以通过调用系统函数来创建、连接、发送和接收数据。常用的套接字类型包括 TCP 套接字和 UDP 套接字,它们分别用于建立可靠的连接和不可靠的连接。
在编程中,需要使用套接字相关的系统函数来进行网络通信。常用的系统函数包括 socket()、bind()、listen()、accept()、connect()、send()和 recv()等。其中,socket()函数用于创建套接字,bind()函数用于将套接字与一个地址绑定,listen()函数用于监听连接请求,accept()函数用于接受连接,connect()函数用于建立连接,send()函数用于发送数据,recv()函数用于接收数据。
网络套接字在 Linux 系统编程中是一个非常重要的概念,它是实现网络通信的基础。了解套接字的概念和相关系统函数的使用方法,可以帮助开发人员编写高效、可靠的网络应用程序。
大小端字节序是指在多字节数据类型(如 int、long、float 等)的存储过程中,字节的存储顺序不同。
在大端字节序中,高位字节存放在低地址,低位字节存放在高地址;
而在小端字节序中,低位字节存放在低地址,高位字节存放在高地址。
2 3
0000 0010 0000 0011
大端: 0100 0000 1100 0000
小端: --------->高地址 低地址
在 Linux 系统编程中,处理器架构可能是大端字节序或小端字节序,因此需要特别注意在网络通信中字节序的处理。
重点:IO 和网络 IO 时永远是低地址的内容先出去
网络通信中通常使用大端字节序(也称为网络字节序)作为标准字节序。为了保证跨平台通信的正确性,需要将本地字节序(即处理器架构字节序)与网络字节序进行转换。可以使用 htonl()、htons()、ntohl()和 ntohs()等系统函数来完成字节序的转换。
//本地字节序转网络字节序
#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);
请见 C++内容中的字节对齐,在网络编程中一般会采取不对齐处理
在 C 中 int、char 等,并没有规定一定的长度大小,可能不同环境下会有不同,为了解决此问题,一般会采取以下固定长度整数类型进行使用
#include <stdint.h>
//int8_t: 8 位有符号整数,定义在 stdint.h 或 cstdint 头文件中
//uint8_t: 8 位无符号整数,定义在 stdint.h 或 cstdint 头文件中
//int16_t: 16 位有符号整数,定义在 stdint.h 或 cstdint 头文件中
//uint16_t: 16 位无符号整数,定义在 stdint.h 或 cstdint 头文件中
//int32_t: 32 位有符号整数,定义在 stdint.h 或 cstdint 头文件中
//uint32_t: 32 位无符号整数,定义在 stdint.h 或 cstdint 头文件中
//int64_t: 64 位有符号整数,定义在 stdint.h 或 cstdint 头文件中
//uint64_t: 64 位无符号整数,定义在 stdint.h 或 cstdint 头文件中
在 Linux 系统编程中,套接字描述符是一种重要的概念,它是一种用于进行网络通信的抽象概念,类似于文件描述符,用于标识和管理网络连接
在 Linux 中,套接字描述符是一个整数,它唯一标识一个网络连接。套接字描述符通常由 socket()函数创建,bind()和 connect()函数绑定和连接网络端点,send()和 recv()函数进行数据传输,close()函数关闭套接字。
socket()函数是用于创建套接字的系统调用。套接字是一种抽象的通信机制,用于在网络上进行数据传输和通信。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
int socket(int domain, int type, int protocol);
-1 返回值:若成功,返回文件描述符;若出错,返回
套接字通信域 domain:
AF_INET IPv4因特网域
AF_INET6 Ipv6因特网域
AF_UNIX UNIX域 AF_UNSPEC 未指定
套接字类型 type:
SOCK_DGRAM 固定长度的、无连接的、不可靠的报文传递
SOCK_RAW IP协议的数据报接口
SOCK_SEQPACKET 固定长度的、有序的、可靠的、面向连接的报文传递 SOCK_STREAM 有序的、可靠的、双向的、面向连接的字节流
为因特网域套接字定义的协议 protocol:
IPPROTO_IP IPv4网际协议
IPPROTO_IPV6 IPv6网际协议
IPPROTO_ICMP 因特网控制报文协议
IPPROTO_RAW 原始IP数据包协议
IPPROTO_TCP 传输控制协议 IPPROTO_UDP 用于数据报协议
创建套接字样例
#include <iostream>
#include <cstring>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <unistd.h>
using namespace std;
int main(int argc, char **argv)
{
//监听套接字创建
int fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (fd == -1)
{
<< "socket error " << strerror(errno) << endl;
cout }
else
{
<< "socket fd = " << fd << endl;
cout }
(fd);
closereturn 0;
}
shutdown 函数是一个系统调用,用于关闭一个已经连接的套接字或禁止其读写操作。它位于头文件<sys/socket.h>中,并具有以下原型:
#include <sys/socket.h>
int shutdown(int socket, int how);
socket 为已经连接的套接字描述符,how 指定关闭的方式:
:关闭套接字的读取功能,即禁止套接字接收数据。
SHUT_RD
SHUT_WR:关闭套接字的写入功能,即禁止套接字发送数据。 SHUT_RDWR:关闭套接字的读写功能,即禁止套接字读取和发送数据。
shutdown 函数可以用于关闭一个已连接的套接字或禁止其读写操作,以此来表示通信结束或出现错误等情况。需要注意的是,调用该函数不会立即关闭套接字,而是将套接字置于一个特殊的状态,直到所有未发送的数据都被发送完毕或者被丢弃为止。在调用 shutdown 函数之后,应用程序仍然需要通过 close 函数关闭套接字描述符。
如果 shutdown 函数调用成功,返回值为 0。如果发生错误,返回值为-1,并设置相应的错误代码,可以通过 errno 变量获取。
进程标识由两部分组成,一部分是计算机的网络地址,它可以帮助标识网络上我们想与之通信的计算机;另一部分是该计算机上用端口号表示的服务,它可以帮助标识特定的进程。
网络地址通常使用 IPv4 或 IPv6 地址格式。IPv4 地址是 32 位的地址,通常表示为四个数字,每个数字之间用点号分隔。例如,192.168.1.1 就是一个 IPv4 地址。
IPv6 地址是 128 位的地址,通常表示为 8 组 4 位十六进制数,每组之间用冒号分隔。例如,2001:0db8:85a3:0000:0000:8a2e:0370:7334 就是一个 IPv6 地址。
此外,还有一些其他的网络地址格式,如 MAC 地址和网络掩码等。MAC 地址是 48 位的地址,通常表示为 12 个十六进制数,每个数之间用冒号或连字符分隔。网络掩码是用于子网划分的一种地址格式,它通常与 IPv4 地址一起使用,表示一个子网的地址范围。
1、struct sockaddr
它是一个抽象结构,其具体实现取决于地址族(address family)的类型。
struct sockaddr {
unsigned short sa_family; // 地址族类型 AF_INET AF_INET6 ...
char sa_data[14]; // 具体的地址信息
};
2、struct sockaddr_in
struct sockaddr_in {
short int sin_family; // 地址族类型,固定为AF_INET
unsigned short int sin_port; // 端口号,网络字节序
struct in_addr sin_addr; // IPv4地址
unsigned char sin_zero[8]; // 暂未使用,一般设为0
};
struct in_addr {
uint32_t s_addr; // IPv4地址,网络字节序
};
/*
struct sockaddr_in* p = (struct sockaddr_in*)&addr;
char* ip = inet_ntoa(p->sin_addr);
printf("IP address: %s\n", ip);
*/
3、struct sockaddr_in6
struct sockaddr_in6 {
uint16_t sin6_family; // 地址族类型,固定为AF_INET6
uint16_t sin6_port; // 端口号,网络字节序
uint32_t sin6_flowinfo; // 流标识,暂未使用
struct in6_addr sin6_addr; // IPv6地址
uint32_t sin6_scope_id; // 作用域标识,暂未使用
};
struct in6_addr {
unsigned char s6_addr[16]; // IPv6地址,使用网络字节序
};
/*
struct sockaddr_in6* p = (struct sockaddr_in6*)&addr;
char ip[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &(p->sin6_addr), ip, INET6_ADDRSTRLEN);
printf("IP address: %s\n", ip);
*/
inet_ntop()函数是一个网络编程中常用的函数,用于将网络字节序的 IP 地址转换为字符串格式
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
其中,af 参数指定了地址族类型,可以是 AF_INET(IPv4)或 AF_INET6(IPv6);src 参数是指向存储 IP 地址的指针;dst 参数是指向存储转换后的字符串的指针;size 参数指定了 dst 指针指向的缓冲区的大小。
inet_ntop()函数成功转换 IP 地址时,返回值是指向转换后的字符串的指针。如果出现错误,返回值为 NULL,并且可以通过调用 errno 来获取错误码。
IPv4 代码样例
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
struct in_addr addr;
.s_addr = htonl(0x7f000001); // 127.0.0.1
addrchar ip[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &addr, ip, INET_ADDRSTRLEN) == NULL)
{
("inet_ntop");
perrorreturn 1;
}
("IP address: %s\n", ip); // IP address: 127.0.0.1
printfreturn 0;
}
IPv6 代码样例
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
struct in6_addr addr;
.s6_addr[0] = 0x20;
addr.s6_addr[1] = 0x01;
addr.s6_addr[2] = 0x0d;
addr.s6_addr[3] = 0xb8;
addrchar ip[INET6_ADDRSTRLEN];
if (inet_ntop(AF_INET6, &addr, ip, INET6_ADDRSTRLEN) == NULL)
{
("inet_ntop");
perrorreturn 1;
}
("IP address: %s\n", ip);
printf// IP address: 2001:db8::f7e5:78a4:ff7f:0
return 0;
}
用于将字符串格式的 IP 地址转换为网络字节序的二进制格式
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
其中,af 参数指定了地址族类型,可以是 AF_INET(IPv4)或 AF_INET6(IPv6);src 参数是指向存储字符串格式 IP 地址的指针;dst 参数是指向存储转换后的二进制格式 IP 地址的指针。
inet_pton()函数成功转换 IP 地址时,返回值为 1。如果出现错误,返回值为 0,并且可以通过调用 errno 来获取错误码。
IPv4 样例
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
const char *ip_str = "127.0.0.1";
struct in_addr addr;
if (inet_pton(AF_INET, ip_str, &addr) <= 0)
{
("inet_pton");
perrorreturn 1;
}
("IP address: 0x%x\n", ntohl(addr.s_addr));
printf// IP address: 0x7f000001
return 0;
}
IPv6 样例
#include <arpa/inet.h>
#include <stdio.h>
int main()
{
const char *ip_str = "2001:db8::";
struct in6_addr addr;
if (inet_pton(AF_INET6, ip_str, &addr) <= 0)
{
("inet_pton");
perrorreturn 1;
}
("IP address: ");
printffor (int i = 0; i < 16; i++)
{
("%02x", addr.s6_addr[i]);
printf}
("\n");
printf// IP address: 2001 0db8 000000000000000000000000
return 0;
}
/etc/hosts 文件是一个文本文件,用于将主机名映射到 IP 地址,或者将别名映射到主机名。在 Linux 和 Unix 系统中,每个主机都有一个/etc/hosts 文件,用于本地解析域名。
/etc/hosts 文件中每一行都表示一条主机名和地址的映射关系,格式如下:
IP地址 主机名 别名
其中,IP 地址是指主机的 IP 地址,主机名是指主机的名称,别名是指主机的别名,可以有多个别名。每个字段之间使用空格或制表符进行分隔。
例如
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateHosts = false
127.0.0.1 localhost
127.0.1.1 DESKTOP-QDLGRDB. DESKTOP-QDLGRDB
192.168.56.1 windows10.microdone.cn
192.168.38.130 master
192.168.0.107 host.docker.internal
192.168.0.107 gateway.docker.internal
127.0.0.1 kubernetes.docker.internal
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
::0 ip6-localnet
fe00::0 ip6-mcastprefix
ff00::1 ip6-allnodes
ff02::2 ip6-allrouters ff02
1、结构体 struct hostent 用于存储主机的信息
struct hostent {
char *h_name; /* official name of host */
char **h_aliases; /* alias list */
int h_addrtype; /* host address type */
int h_length; /* length of address */
char **h_addr_list; /* list of addresses */
};
//h_name:官方主机名,也就是主机的正式名称。
//h_aliases:别名列表,一个指向字符指针数组的指针,其中每个元素都是一个指向主机的别名的字符指针。
//h_addrtype:主机地址类型,通常为AF_INET(IPv4地址)或AF_INET6(IPv6地址)。
//h_length:地址长度,通常为4(IPv4地址长度)或16(IPv6地址长度)。
//h_addr_list:地址列表,一个指向字符指针数组的指针,其中每个元素都是一个指向主机地址的字符指针。
2、gethostent、sethostent、endhostent 函数
#include <iostream>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using std::cout;
using std::endl;
int main()
{
struct hostent *host;
(1);//1表示保持打开/etc/hosts
sethostentwhile ((host = gethostent()) != NULL)
{
<< "host: " << host->h_name << endl;
cout // 别名
char **alias;
for (alias = host->h_aliases; *alias != NULL; alias++)
{
<< "alias: " << *alias << endl;
cout }
// 地址
char **addr;
for (addr = host->h_addr_list; *addr != NULL; addr++)
{
struct in_addr *ptr = (struct in_addr *)(*addr);
<< "addr: " << inet_ntoa(*ptr) << endl;
cout }
}
(); // 关闭/etc/hosts文件
endhostentreturn 0;
}
/*
host: localhost
addr: 127.0.0.1
host: DESKTOP-QDLGRDB.
alias: DESKTOP-QDLGRDB
addr: 127.0.1.1
host: windows10.microdone.cn
addr: 192.168.56.1
host: master
addr: 192.168.38.130
host: host.docker.internal
addr: 192.168.0.107
host: gateway.docker.internal
addr: 192.168.0.107
host: kubernetes.docker.internal
addr: 127.0.0.1
host: ip6-localhost
alias: ip6-loopback
addr: 127.0.0.1
*/
/etc/networks 是一个文本文件,包含已知网络的名称、网络号、子网掩码等信息,是一个网络数据库(network database),用于在系统中解析网络名称和地址
文件内容格式
[netmask]
networkname network_number //其中,networkname是网络的名称,network_number是网络的地址,netmask是网络的子网掩码。网络地址和子网掩码可以是点分十进制或十六进制格式的,如果省略了子网掩码,则默认为类别掩码(classful mask)。
例如
127 # loopback network
loopback -local 169.254 # link-local network for address autoconfiguration
link192.168.0 # local network localnet
1、结构体 struct netent
struct netent {
char *n_name; // 网络名称
char **n_aliases; // 网络别名列表,以NULL结尾
int n_addrtype; // 地址类型,通常为AF_INET
uint32_t n_net; // 网络地址,以网络字节序存储
};
//n_name表示网络的名称
//n_aliases是一个指向网络别名列表的指针,列表以NUL尾
//n_addrtype表示地址类型,通常为AF_INET(IPv4)
//n_net表示网络的地址,以网络字节序(network byte order)存储。
2、getnetbyaddr、getnetbyname、getnetent、setnetent、 endnetent 函数
getnetbyaddr 函数用于获取指定网络地址的网络条目信息。net 参数是网络地址,以网络字节序存储,type 参数表示地址类型,通常为 AF_INET(IPv4)。如果找到对应的网络条目,则返回一个指向 struct netent 结构体的指针,否则返回 NULL。
#include <netdb.h>
struct netent *getnetbyaddr(uint32_t net, int type);
getnetbyname 函数用于获取指定网络名称的网络条目信息。name 参数是网络名称。如果找到对应的网络条目,则返回一个指向 struct netent 结构体的指针,否则返回 NULL。
#include <netdb.h>
struct netent *getnetbyname(const char *name);
getnetent 函数用于按顺序读取网络数据库中的每个条目。如果找到下一个网络条目,则返回一个指向 struct netent 结构体的指针,否则返回 NULL。
#include <netdb.h>
struct netent *getnetent(void);
setnetent 函数用于打开网络数据库文件/etc/networks,并将文件指针设置为文件开头,以便逐条读取网络条目信息。如果 stayopen 参数不为 0,则保持数据库打开状态以供后续函数使用;否则在每次打开和读取时打开和关闭数据库。
#include <netdb.h>
void setnetent(int stayopen);
endnetent 函数用于关闭网络数据库文件/etc/networks,以便释放资源。
#include <netdb.h>
void endnetent(void);
综合样例
#include <iostream>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using std::cout;
using std::endl;
int main()
{
char ip[INET_ADDRSTRLEN];
struct netent *ptr;
(1);
setnetentwhile ((ptr = getnetent()) != NULL)
{
<< "name: " << ptr->n_name << endl;
cout << "address: " << inet_ntop(AF_INET, (void *)&ptr->n_net, ip, INET_ADDRSTRLEN) << endl;
cout << "aliases:" << endl;
cout char **alias = ptr->n_aliases;
while (*alias != NULL)
{
<< *alias << endl;
cout ++;
alias}
}
// getnetbyaddr
uint32_t net_addr = inet_addr("169.254.0.0");
= getnetbyaddr(net_addr, AF_INET);
ptr if (ptr != NULL)
{
<< "name: " << ptr->n_name << endl;
cout }
// getnetbyname
= getnetbyname("link-local");
ptr if (ptr != NULL)
{
<< "address: " << inet_ntop(AF_INET, (void *)&ptr->n_net, ip, INET_ADDRSTRLEN) << endl;
cout }
();
endnetentreturn 0;
}
/*
name: link-local
address: 0.0.254.169
aliases:
address: 0.0.254.169
*/
系统的协议数据库文件,即/etc/protocols 文件。该文件是一个文本文件,其中每一行表示一个协议条目,具有如下格式:
<protocol name> <protocol number> <alias list>
其中,
例如
1 ICMP
icmp 2 IGMP
igmp 6 TCP
tcp 17 UDP udp
1、struct protent
struct protoent {
char *p_name; /* 协议名称 */
char **p_aliases; /* 协议别名列表 */
int p_proto; /* 协议编号 */
};
2、getprotobyname、getprotobynumber、getprotoent、setprotoent、endprotoent 函数
函数的使用方式都是类似的,下面是一个综合用例
#include <iostream>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using std::cout;
using std::endl;
int main()
{
struct protoent *ptr;
(1);
setprotoentwhile ((ptr = getprotoent()) != NULL)
{
<< "name: " << ptr->p_name << " number: " << ptr->p_proto << endl;
cout }
= getprotobyname("TCP");
ptr if (ptr != NULL)
{
<< "name: " << ptr->p_name << " number: " << ptr->p_proto << endl;
cout }
= getprotobynumber(6);
ptr if (ptr != NULL)
{
<< "name: " << ptr->p_name << " number: " << ptr->p_proto << endl;
cout }
();
endprotoentreturn 0;
}
/etc/services 文件,存储服务条目的信息,其每一行表示一个服务条目
<service name> <port number>/<protocol name> <alias list>
其中,
例如
-data 20/tcp
ftp21/tcp
ftp 22/tcp
ssh 23/tcp
telnet 25/tcp mail
smtp 80/tcp www httpd http
1、struct servent
struct servent {
char *s_name; /* 服务名称 */
char **s_aliases; /* 服务别名列表 */
int s_port; /* 端口号 */
char *s_proto; /* 协议名称 */
};
2、getservbyname、getserbyport、getservent、setservent、endservent 函数
综合使用样例
#include <iostream>
#include <netdb.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using std::cout;
using std::endl;
int main()
{
struct servent *ptr;
(1);
setserventwhile ((ptr = getservent()) != NULL)
{
<< ptr->s_name << " " << ptr->s_port << "/" << ptr->s_proto << endl;
cout }
= getservbyname("echo", "tcp");
ptr if (ptr != NULL)
{
<< ptr->s_name << " " << ptr->s_port << "/" << ptr->s_proto << endl;
cout }
= getservbyport(256, "tcp");
ptr if (ptr != NULL)
{
<< ptr->s_name << " " << ptr->s_port << "/" << ptr->s_proto << endl;
cout }
();
endserventreturn 0;
}
getaddrinfo 可以很好的代替 gethostbyname 和 gethostbyaddr 函数,其作用为知道主机名或服务名,查找其地址结构信息
man 手册解释
getaddrinfo, freeaddrinfo, gai_strerror - network address and service translation
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc.
int ai_family; // AF_UNSPEC, AF_INET, AF_INET6, etc.
int ai_socktype; // SOCK_STREAM, SOCK_DGRAM, SOCK_RAW, etc.
int ai_protocol; // IPPROTO_TCP, IPPROTO_UDP, IPPROTO_RAW, etc.
size_t ai_addrlen; // length of ai_addr
struct sockaddr *ai_addr; // struct sockaddr_in or _in6
char *ai_canonname; // canonical name for nodename
struct addrinfo *ai_next; // linked list, next node
};
/*
ai_flags选项
AI_ADDRCONFIG 查询配置的地址类型(IPv4和IPv6)
AI_ALL 查找IPv4和IPv6地址(仅用于AI_V4MAPPED)
AI_CANONNAME 需要一个规范的名字(与别名相对)
AI_NUMBERICHOST 以数字格式指定主机地址,不翻译
AI_NUMBERICSERV 将服务指定为数字端口号,不翻译
AI_PASSIVE 套接字地址用于监听绑定
AI_V4MAPPED 如果没有找到IPV6地址,返回映射到IPv6格式的IPv4地址
*/
相关函数原型
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);
void freeaddrinfo(struct addrinfo *res);
样例,查找符合 hint 条件的主机名为”DESKTOP-QDLGRDB.”对应的 IP 地址
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <cstring>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
struct addrinfo hints; // 搜索限制参数
struct addrinfo *result, *rp;
int s;
/* 配置 addrinfo 结构体 */
(&hints, 0, sizeof(hints));
memset.ai_family = AF_UNSPEC; /* IPv4 或 IPv6 */
hints.ai_socktype = SOCK_STREAM; /* 流式套接字 */
hints
/* 获取主机名对应的 IP 地址列表 */
= getaddrinfo("DESKTOP-QDLGRDB.", NULL, &hints, &result);
s if (s != 0)
{
(stderr, "getaddrinfo: %s\n", gai_strerror(s));
fprintf(EXIT_FAILURE);
exit}
/* 遍历获取的地址列表 */
for (rp = result; rp != NULL; rp = rp->ai_next)
{
void *addr;
char ip_str[INET6_ADDRSTRLEN];
if (rp->ai_family == AF_INET)
{ /* IPv4 */
struct sockaddr_in *ipv4 = (struct sockaddr_in *)rp->ai_addr;
= &(ipv4->sin_addr);
addr }
else
{ /* IPv6 */
struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)rp->ai_addr;
= &(ipv6->sin6_addr);
addr }
/* 将地址转换为字符串 */
if (inet_ntop(rp->ai_family, addr, ip_str, INET6_ADDRSTRLEN) == NULL)
{
("inet_ntop");
perror(EXIT_FAILURE);
exit}
/* 打印地址 */
("%s\n", ip_str);
printf}
/* 释放地址列表 */
(result);
freeaddrinforeturn 0;
}
如果 getaddrinfo 失败,不能使用 perror 或 strerror 生成错误信息,需要使用 gai_strerror 将返回的错误码转换成错误信息
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
const char *gai_strerror(int errcode);
样例请见上一个样例中的使用
getnameinfo 用于将一个地址转换成一个主机名和一个服务名
#include <sys/socket.h>
#include <netdb.h>
int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);
/*flags参数选择:
NI_DGRAM 服务基于数据报而非基于流
NI_NAMEREQD 如果找不到主机名,将其作为一个错误对待
NI_NOFQDN 对于本地主机,仅返回全限定域名的节点名部分
NI_NUMERICHOST 返回主机地址的数字形式,而非主机名
NI_NUMERICSCOPE 对于IPv6,返回范围ID的数字形式,而非名字
NI_NUMERICSERV 返回服务地址的数字形式(即端口号),而非名字
NI_NUMERICHOST 如果不能解析名称,则返回包含该地址的数字字符串。
NI_IDN 如果主机名使用国际化域名,则进行IDN编码。
*/
使用样例
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
struct sockaddr_in addr;
(&addr, 0, sizeof(addr));
memset.sin_family = AF_INET;
addr(AF_INET, "192.168.0.107", &addr.sin_addr);
inet_ptonchar host[NI_MAXHOST];
char service[NI_MAXSERV];
int flags = 0; // NI_NUMERICSERV | NI_NUMERICHOST;
int ret = getnameinfo((struct sockaddr *)&addr, sizeof(addr),
, NI_MAXHOST, service, NI_MAXSERV, flags);
hostif (ret != 0)
{
(stderr, "getnameinfo: %s\n", gai_strerror(ret));
fprintfreturn 1;
}
("Host: %s\n", host); // Host: host.docker.internal
printf("Service: %s\n", service); // Service: 0
printfreturn 0;
}
使用 bind 函数来关联地址和套接字,即将套接字和 IP 地址与端口号绑定起来,使得套接字可以接受特定地址发来的数据报
// bind a name to a socket
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
注意事项:
1、指定的地址必须有效,在进程运行的主机上应该有效
2、地址必须和创建套接字时的地址族所支持的格式相匹配
3、地址端口一般不小于 1024,除非该进程具有相应权限
4、一般只能将一个套接字端点绑定到一个给定地址上,尽管有些协议允许多重绑定
5、bind 函数之前必须先创建套接字,并且套接字处于未连接状态,对于 TCP
套接字必须出与 LISTEN 状态之前
6、如果要绑定的地址为 INADDR_ANY 或
IN6ADDR_ANY,则表示该套接字可以接受来自任何 IP 地址的数据包
7、在多次使用同一个地址和端口号绑定套接字时,必须使用 SO_REUSEADDR
套接字选项,否则会报错
简单使用样例
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
using namespace std;
int main(int argc, char **argv)
{
int socketfd;
struct sockaddr_in server_addr; // IPv4
// 创建套接字
= socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // TCP
socketfd if (socketfd < 0)
{
<< "socket " << strerror(errno) << endl;
cerr (EXIT_FAILURE);
exit}
// 绑定地址
(&server_addr, 0, sizeof(server_addr));
memset.sin_family = AF_INET;
server_addr.sin_port = htons(20023);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addrif (bind(socketfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
{
<< "bind " << strerror(errno) << endl;
cerr (socketfd);
close(EXIT_FAILURE);
exit}
<< "server started,listening on port " << 20023 << endl;
cout //...
(socketfd);
closereturn 0;
}
SO_REUSEADDR 是一个 socket 选项,用于在 bind() 函数中重用本地地址(IP 地址和端口号),即使之前连接仍然存在于 TIME_WAIT 状态。这个选项通常在服务器程序中使用,以便能够更快地重启服务器。
当一个 TCP 连接被关闭时,它可能处于 TIME_WAIT 状态,这是因为 TCP 协议需要确保该连接的所有数据都已传输完成,并且双方都已经确认了这一点。这个状态默认持续 2 * MSL(最大报文段生存时间,通常为 30 秒) 的时间,以确保在这段时间内任何迟到的数据包都能够被丢弃。
但是,这个状态下的端口无法被重复使用,这可能会导致服务器无法在短时间内重启,因为旧的连接仍然存在于 TIME_WAIT 状态。使用 SO_REUSEADDR 选项可以绕过这个问题,从而使服务器可以更快地重启并绑定到相同的地址和端口。
需要注意的是,使用 SO_REUSEADDR 选项会有一些安全风险。因为它可以绕过 TIME_WAIT 状态下的端口使用限制,因此可能会导致新的连接接收到旧的数据包,或者让攻击者冒充旧连接的一方。因此,使用该选项时需要仔细评估安全风险,并且只在确保安全的情况下使用。
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1) {
("socket");
perror(1);
exit}
int optval = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0) {
("setsockopt");
perror(1);
exit}
TIME_WAIT 状态是指在套接字连接断开之后,等待一段时间以确保所有分组都被正确处理之后才彻底关闭套接字。在这个状态下,套接字不能被重新使用,直到经过足够长的时间。
在套接字编程中,TIME_WAIT 状态通常发生在主动关闭连接的一方,即调用 close() 函数来关闭连接的一方。在关闭连接之后,套接字会进入 TIME_WAIT 状态并等待 2MSL 的时间(MSL 是 Maximum Segment Lifetime 的缩写,代表一个 TCP 分组在网络中最大存活时间)。
TIME_WAIT 状态主要是为了保证网络传输的可靠性和数据完整性。当 TCP 连接的一端发送了 FIN 报文后,进入了 FIN_WAIT_1 状态,并等待另一端的 ACK 报文。如果这个 ACK 报文因为网络原因丢失了,那么这个连接就会一直处于 FIN_WAIT_1 状态,直到超时,这样会使得另一端的 TCP 无法再次连接这个端口,从而导致网络阻塞。为了避免这种情况的发生,TCP 协议规定当一个 TCP 连接被关闭时,必须经过一段时间的 TIME_WAIT 状态,这个状态下,TCP 连接仍然保持着最后一次传输的数据,以及最后一次发送和接收到的序列号等信息,如果另一端有需要,就可以进行重传,从而保证数据的可靠传输。因此,TIME_WAIT 状态对于网络传输的可靠性和数据完整性至关重要。
用于获取一个已绑定的套接字的本地地址信息,包括 IP 地址和端口号。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//函数返回值为 0 表示成功,-1 表示出错。
使用样例
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
("socket creation failed");
perror(EXIT_FAILURE);
exit}
struct sockaddr_in addr;
.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(12345);
addrif (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
("bind failed");
perror(EXIT_FAILURE);
exit}
struct sockaddr_in local_addr;
socklen_t addr_len = sizeof(local_addr);
if (getsockname(sockfd, (struct sockaddr*)&local_addr, &addr_len) < 0) {
("getsockname failed");
perror(EXIT_FAILURE);
exit}
char ip_str[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &(local_addr.sin_addr), ip_str, INET_ADDRSTRLEN) == NULL) {
("inet_ntop failed");
perror(EXIT_FAILURE);
exit}
("Local IP address: %s\n", ip_str);
printf("Local port: %d\n", ntohs(local_addr.sin_port));
printf(sockfd);
closereturn 0;
}
用于获取已连接套接字的远程地址信息
#include <sys/socket.h>
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//若调用成功,返回0,addr 存储了对端套接字的地址信息,addrlen 已经被更新为 addr 结构体的实际长度。
//若出现错误,返回-1,并设置 errno 变量来指示错误类型。
使用样例 demo
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd, newsockfd, portno;
socklen_t clilen;
char buffer[256];
struct sockaddr_in serv_addr, cli_addr;
int n;
= socket(AF_INET, SOCK_STREAM, 0);
sockfd if (sockfd < 0)
{
("ERROR opening socket");
perror(1);
exit}
((char *)&serv_addr, sizeof(serv_addr));
bzero= 5001;
portno
.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
serv_addr
if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
("ERROR on binding");
perror(1);
exit}
(sockfd, 5);
listen
= sizeof(cli_addr);
clilen = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);
newsockfd if (newsockfd < 0)
{
("ERROR on accept");
perror(1);
exit}
struct sockaddr_in remote_addr;
socklen_t addrlen = sizeof(remote_addr);
(newsockfd, (struct sockaddr *)&remote_addr, &addrlen);
getpeername
("Connection accepted from %s:%d\n", inet_ntoa(remote_addr.sin_addr), ntohs(remote_addr.sin_port));
printf
(newsockfd);
close(sockfd);
closereturn 0;
}
常见的状态
CLOSED:初始状态,未连接
LISTEN:套接字正在监听连接请求
SYN_SENT:向对方发起连接请求,并等待对方确认
SYN_RECEIVED:接收到对方的连接请求,并已发送确认
ESTABLISHED:连接已建立,双方可以进行通信
FIN_WAIT_1:关闭连接,等待对方发送关闭请求
FIN_WAIT_2:等待对方发送关闭请求或确认已发送的关闭请求
CLOSE_WAIT:等待对方发送关闭请求
CLOSING:发送关闭请求,并等待对方的确认
LAST_ACK:等待对方的关闭请求的确认
TIME_WAIT:等待一段时间,确保对方已收到自己发送的关闭请求的确认 CLOSED:连接已关闭,处于最终状态
常见的状态转换
客户端 服务器
socket SYN J,MSS=536
connect(阻塞)-------------------> socket,bind,listen(LISTEN)
SYN_SENT accept(阻塞) SYN_RCVD
<-------------------
SYN K,ACK J+1 MSS=1460
ESTABLISHED
connect返回 ---------------------> ESTABLISHED accept返回,read(阻塞)
ACK K+1
write
read阻塞 ----------------------> read 返回
数据请求
read返回 <--------------------- write read阻塞
数据应答,请求ACK
--------------------->
应答ACK
close主动关闭 ---------------------> CLOSE_WAIT(被动关闭) read返回0
FIN_WAIT_1 FIN M
FIN_WAIT2 <--------------------
ACK M+1
TIME_WAIT <---------------------close LAST_ACK
FIN N
----------------------> CLOSED
ACK N+1
(2MSL)CLOSED
特殊的CLOSING状态:对于主动关闭方,CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文
用于建立与远程主机的连接。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
调用 connect 函数会使系统尝试与远程主机建立连接,如果连接建立成功,则返回 0。如果连接建立失败,则返回-1,并设置相应的错误码,可以通过 perror 函数来输出错误信息。
需要注意的是,如果 connect 函数返回-1 并设置了错误码 EINPROGRESS,则说明连接建立请求已经被发送到远程主机,但连接还未建立完成。此时可以调用 select 函数来等待连接完成,或者调用 poll 函数、epoll_wait 函数等待连接完成。
另外,如果使用非阻塞的套接字进行连接操作,也需要先调用 connect 函数,然后通过 select 函数等待连接完成。在这种情况下,connect 函数可能会立即返回,并设置错误码为 EINPROGRESS。
样例 demo
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 20023
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
("socket");
perror(EXIT_FAILURE);
exit}
struct sockaddr_in servaddr;
(&servaddr, 0, sizeof(servaddr));
memset
.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr
if (inet_pton(AF_INET, "61.171.51.135", &servaddr.sin_addr) <= 0)
{
("inet_pton");
perror(EXIT_FAILURE);
exit}
if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
{
("connect");
perror(EXIT_FAILURE);
exit}
("Connected to server\n");
printf
(sockfd);
closereturn 0;
}
用于将指定的套接字设置为从客户端接受连接请求的状态
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
/*
sockfd:需要设置为监听状态的套接字描述符。
backlog:内核为相应套接字排队的最大连接个数。
该函数调用成功时返回0,失败时返回-1,并设置errno为相应的错误码。
backlog: 它指定了可以在已完成连接队列中等待接受的尚未被 accept 函数处理的连接请求的数量。
*/
样例 demo
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#define SERVER_PORT 8080
#define BACKLOG 5
int main() {
int listenfd, connfd;
struct sockaddr_in servaddr, cliaddr;
socklen_t clilen;
// 创建监听套接字
= socket(AF_INET, SOCK_STREAM, 0);
listenfd if (listenfd == -1) {
("socket error");
perror(EXIT_FAILURE);
exit}
// 初始化服务器地址结构体
(&servaddr, sizeof(servaddr));
bzero.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERVER_PORT);
servaddr// 将套接字绑定到服务器地址结构体上
if (bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) == -1) {
("bind error");
perror(EXIT_FAILURE);
exit}
// 将套接字设置为监听状态,等待客户端连接请求
if (listen(listenfd, BACKLOG) == -1) {
("listen error");
perror(EXIT_FAILURE);
exit}
// 处理客户端连接请求
while (1) {
= sizeof(cliaddr);
clilen = accept(listenfd, (struct sockaddr *) &cliaddr, &clilen);
connfd if (connfd == -1) {
("accept error");
perror(EXIT_FAILURE);
exit}
// 处理连接
// ...
// 关闭连接
(connfd);
close}
// 关闭监听套接字
(listenfd);
closereturn 0;
}
accept 函数用于从已经建立连接的套接字队列中取出一个客户端连接,创建一个新的套接字并返回该套接字的文件描述符,用于与客户端进行通信。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/*
sockfd:表示监听套接字的文件描述符;
addr:表示用于存储连接到的客户端地址信息的指针,通常是一个 struct sockaddr_in 类型的指针;
addrlen:表示 addr 指针指向的地址结构体的长度,需要传入一个指向 socklen_t 类型的变量的指针。
如果连接成功,返回一个新的套接字的文件描述符,这个套接字用于与客户端进行通信;
如果出错,返回 -1。
*/
如果 sockfd 处于非阻塞模式,accept 返回-1,并将 errno 设置为 EAGAIN 或 EWOULDBLOCK
使用样例可见上一个样例
在套接字的数据传输中,完全可以使用 read 与 write 函数,如果想指定选项,从多少个客户端接收数据包,或者发送带外数据,就需要使用 6 个为数据传递而设计的套接字函数
send 函数用于发送数据到已连接的套接字
send 函数的返回值是发送数据的实际字节数,如果发生错误,则返回 -1,并设置 errno 变量来指示错误类型。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
/*
sockfd:已连接的套接字描述符。
buf:发送数据缓冲区的指针。
len:发送数据缓冲区的大小。
flags:可选参数,指定发送数据的行为。
flags:
MSG_CONFIRM 提供链路层反馈以保持地址映射有效
MSG_DONTROUTE 勿将数据包路由出本地网络
MSG_DONTWAIT 允许非阻塞操作(等价于使用O_NONBLOCK)
MSG_EOF 发送数据后关闭套接字的发送端
MSG_EOR 如果协议支持,标记记录结束
MSG_MORE 延迟发送数据包允许写更多数据
MSG_NOSIGNAL 在写无连接的套接字时不产生SIGPIPE信息
MSG_OOB 如果协议支持,发送带外数据
*/
注意事项
应该检查返回值,以确保所有数据都被发送出去了。
如果需要,应该在调用 send 函数之前设置套接字选项。
应该考虑对数据进行分段处理,以免发送的数据过大。
#include <sys/socket.h>
#include <unistd.h>
#define BUF_SIZE 1024
int main(int argc, char *argv[]) {
int sockfd;
char buf[BUF_SIZE];
ssize_t bytes_sent;
// 创建套接字并连接到远程主机
= socket(AF_INET, SOCK_STREAM, 0);
sockfd // ...
// connect(sockfd, ...);
// 发送数据
= send(sockfd, buf, BUF_SIZE, 0);
bytes_sent if (bytes_sent == -1) {
("send error");
perrorreturn -1;
}
// 关闭套接字
(sockfd);
closereturn 0;
}
sendto 函数用于 DUP 套接字发送数据的函数
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
/*
sockfd:已连接套接字的文件描述符。
buf:指向要发送数据的缓冲区。
len:发送数据的长度。
flags:额外的选项,可以设置为 0。与send的参数类似。
dest_addr:目标地址结构体的指针,包含了目标 IP 和端口信息。
addrlen:目标地址结构体的大小。
*/
使用 sendto() 函数发送数据,一般不需要事先建立连接。如果已经建立了连接,也可以使用该函数发送数据,只需要忽略 dest_addr 和 addrlen 参数即可。
函数返回值为发送的字节数,如果出现错误,返回值为 -1。可以通过 errno 全局变量获取错误代码。
sendto 函数使用样例
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#define PORT 12345
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[] = "Hello, World!";
// 创建套接字
= socket(AF_INET, SOCK_DGRAM, 0);
sockfd // 设置服务器地址
.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr(&(server_addr.sin_zero), 0, 8);
memset// 发送数据
(sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&server_addr, sizeof(struct sockaddr));
sendto// 关闭套接字
(sockfd);
closereturn 0;
}
再通过套接字发送数据时,可以调用带有 msghdr 结构的 sendmsg 来指定多重缓冲区传输数据,和 writev 类似
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
struct msghdr
struct iovec {
void *iov_base; // 数据缓冲区的起始地址
size_t iov_len; // 数据缓冲区的长度
};
struct msghdr
{
void *msg_name; /* Optional address */
socklen_t msg_namelen; /* Size of address */
struct iovec *msg_iov; /* Scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* Ancillary data, see below */
size_t msg_controllen; /* Ancillary data buffer len */
int msg_flags; /* Flags (unused) */
};
/*
void *msg_name:一个指向存放目标协议地址的缓冲区的指针。在发送数据时,这个字段可以为 NULL。在接收数据时,这个字段指向一个套接字地址结构体,例如 struct sockaddr_in 或 struct sockaddr_un。
socklen_t msg_namelen:存放目标协议地址缓冲区的长度。这个字段只在 msg_name 不为 NULL 时有用。
struct iovec *msg_iov:一个指向 struct iovec 结构体的指针,用于指定要发送或接收的数据缓冲区和缓冲区大小。
int msg_iovlen:msg_iov 数组中元素的个数。
void *msg_control:一个指向辅助数据的指针,例如控制消息。在发送和接收数据时,这个字段通常为 NULL。
socklen_t msg_controllen:辅助数据的长度。
int msg_flags:一组标志位,用于指定如何处理消息。
*/
用于从套接字接收数据的函数
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
/*
sockfd:表示要接收数据的套接字文件描述符;
buf:表示接收数据的缓冲区地址;
len:表示缓冲区长度;
flags:通常设置为0,表示默认行为。
recv函数的返回值为实际接收到的字节数,如果返回0表示对端已关闭连接,如果返回-1表示出现错误,此时需要通过errno变量查看具体错误原因。
*/
flags 参数
MSG_CMSG_CLOEXEC 为UNIX域套接字上接收的文件描述符设置执行时关闭标志
MSG_DONTWAIT 启用非阻塞操作(相当于使用O_NONBLOCK)
MSG_ERRQUEUE 接收错误信息作为辅助数据
MSG_OOB 如果协议支持,获取带外数据
MSG_PEEK 返回数据包内容而不真正取走数据包
MSG_TRUNC 即使数据包被阶段,也返回数据包的实际长度 MSG_WAITALL 等待直到所有的数据可用(仅SOCK_STREAM)
样例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
int main()
{
int sockfd, connfd, n;
char buf[BUF_SIZE];
struct sockaddr_in servaddr;
// 创建套接字
= socket(AF_INET, SOCK_STREAM, 0);
sockfd if (sockfd == -1) {
("socket error");
perror(EXIT_FAILURE);
exit}
// 设置服务器地址
.sin_family = AF_INET;
servaddr.sin_port = htons(12345);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr// 绑定套接字
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
("bind error");
perror(EXIT_FAILURE);
exit}
// 监听套接字
if (listen(sockfd, 10) == -1) {
("listen error");
perror(EXIT_FAILURE);
exit}
// 接受客户端连接
= accept(sockfd, NULL, NULL);
connfd if (connfd == -1) {
("accept error");
perror(EXIT_FAILURE);
exit}
// 接收客户端发送的数据
while ((n = recv(connfd, buf, BUF_SIZE, 0)) > 0) {
("receive %d bytes: %s\n", n, buf);
printf}
// 关闭连接
(connfd);
close(sockfd);
closereturn 0;
}
recvfrom() 函数是在 Unix 系统上用于接收数据的函数,特别适用于网络编程中的套接字通信。它用于从一个已连接或未连接的套接字接收数据,并将数据存储在指定的缓冲区中。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
recvfrom() 函数的返回值表示实际接收到的字节数,如果出现错误,则返回 -1。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#define PORT 8080
#define MAX_BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in serverAddr, clientAddr;
socklen_t addrLen = sizeof(clientAddr);
char buffer[MAX_BUFFER_SIZE];
// 创建套接字
= socket(AF_INET, SOCK_DGRAM, 0);
sockfd if (sockfd < 0) {
("socket creation failed");
perrorreturn -1;
}
// 绑定套接字到指定端口
(&serverAddr, 0, sizeof(serverAddr));
memset.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(PORT);
serverAddrif (bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
("bind failed");
perrorreturn -1;
}
// 接收数据
ssize_t numBytes = recvfrom(sockfd, buffer, MAX_BUFFER_SIZE, 0, (struct sockaddr*)&clientAddr, &addrLen);
if (numBytes < 0) {
("recvfrom failed");
perrorreturn -1;
}
// 打印接收到的数据
("Received data: %s\n", buffer);
printf// 关闭套接字
(sockfd);
closereturn 0;
}
在这个示例中,我们创建了一个 UDP 服务器,通过 recvfrom() 函数从客户端接收数据,并将接收到的数据打印出来。
recvmsg() 函数是在 Linux 系统上用于接收消息的函数,特别适用于网络编程中的套接字通信。它可以接收包含多个数据块的消息,并将消息的各个部分存储在指定的缓冲区中。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
参数说明:
struct msghdr {
void *msg_name; // 指向发送方的地址信息的结构体指针
socklen_t msg_namelen; // 发送方地址信息的长度
struct iovec *msg_iov; // 指向数据块的结构体指针数组
int msg_iovlen; // 数据块的数量
void *msg_control; // 指向辅助数据的指针
socklen_t msg_controllen; // 辅助数据的长度
int msg_flags; // 接收消息的标志
};
recvmsg() 函数的返回值表示实际接收到的字节数,如果出现错误,则返回 -1。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#define PORT 8080
#define MAX_BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in serverAddr, clientAddr;
socklen_t addrLen = sizeof(clientAddr);
char buffer[MAX_BUFFER_SIZE];
struct msghdr msg;
struct iovec iov[1];
// 创建套接字
= socket(AF_INET, SOCK_DGRAM, 0);
sockfd if (sockfd < 0) {
("socket creation failed");
perrorreturn -1;
}
// 绑定套接字到指定端口
(&serverAddr, 0, sizeof(serverAddr));
memset.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(PORT);
serverAddrif (bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
("bind failed");
perrorreturn -1;
}
// 接收消息
(&msg, 0, sizeof(msg));
memset(&iov, 0, sizeof(iov));
memset[0].iov_base = buffer;
iov[0].iov_len = sizeof(buffer);
iov.msg_iov = iov;
msg.msg_iovlen = 1;
msgssize_t numBytes = recvmsg(sockfd, &msg, 0);
if (numBytes < 0) {
("recvmsg failed");
perrorreturn -1;
}
// 打印接收到的消息
("Received message: %s\n", (char*)iov[0].iov_base);
printf// 关闭套接字
(sockfd);
closereturn 0;
}
在这个示例中,我们创建了一个 UDP 服务器,通过 recvmsg() 函数从客户端接收消息,并将消息打印出来。
在这个示例中,我们创建了一个 UDP 服务器,通过 recvmsg() 函数从客户端接收消息,并将消息打印出来。
常用的套接字选项,除此之外有很多
套接字选项可以通过 setsockopt() 函数设置
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORT 8080
int main() {
int sockfd;
struct sockaddr_in serverAddr;
// 创建套接字
= socket(AF_INET, SOCK_STREAM, 0);
sockfd if (sockfd < 0) {
("socket creation failed");
perrorreturn -1;
}
// 设置 SO_REUSEADDR 选项
int reuse = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
("setsockopt failed");
perrorreturn -1;
}
// 获取 SO_REUSEADDR 选项的值
int reuseVal;
socklen_t reuseLen = sizeof(reuseVal);
if (getsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuseVal, &reuseLen) < 0) {
("getsockopt failed");
perrorreturn -1;
}
("SO_REUSEADDR value: %d\n", reuseVal);
printf// 关闭套接字
(sockfd);
closereturn 0;
}
getsockopt() 函数是一个用于获取套接字选项值的函数。它可以用来查询套接字的属性和状态。
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORT 8080
int main() {
int sockfd;
struct sockaddr_in serverAddr;
// 创建套接字
= socket(AF_INET, SOCK_STREAM, 0);
sockfd if (sockfd < 0) {
("socket creation failed");
perrorreturn -1;
}
// 获取 SO_REUSEADDR 选项的值
int reuseVal;
socklen_t reuseLen = sizeof(reuseVal);
if (getsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuseVal, &reuseLen) < 0) {
("getsockopt failed");
perrorreturn -1;
}
("SO_REUSEADDR value: %d\n", reuseVal);
printf// 关闭套接字
(sockfd);
closereturn 0;
}
在 Linux C++ socket 编程中,带外数据(Out-of-Band data)是指在正常数据流之外传输的特殊数据。带外数据通常用于发送紧急信息或指示特殊事件的发生。TCP 将带外数据称为紧急数据。
在套接字编程中,可以使用 send() 函数的 MSG_OOB 标志来发送带外数据,使用 recv() 函数的 MSG_OOB 标志来接收带外数据。
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORT 8080
int main() {
int sockfd, newsockfd;
struct sockaddr_in serverAddr, clientAddr;
socklen_t addr_size;
char buffer[1024];
// 创建套接字
= socket(AF_INET, SOCK_STREAM, 0);
sockfd if (sockfd < 0) {
("socket creation failed");
perrorreturn -1;
}
// 设置套接字选项,允许发送和接收带外数据
int oobInline = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_OOBINLINE, &oobInline, sizeof(oobInline)) < 0) {
("setsockopt failed");
perrorreturn -1;
}
// 绑定套接字到端口
.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr(serverAddr.sin_zero, '\0', sizeof(serverAddr.sin_zero));
memsetif (bind(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
("bind failed");
perrorreturn -1;
}
// 监听连接
if (listen(sockfd, 10) < 0) {
("listen failed");
perrorreturn -1;
}
// 接受连接
= sizeof(clientAddr);
addr_size = accept(sockfd, (struct sockaddr*)&clientAddr, &addr_size);
newsockfd if (newsockfd < 0) {
("accept failed");
perrorreturn -1;
}
// 接收带外数据
(buffer, '\0', sizeof(buffer));
memsetif (recv(newsockfd, buffer, sizeof(buffer), MSG_OOB) < 0) {
("recv OOB failed");
perrorreturn -1;
}
("Received OOB data: %s\n", buffer);
printf// 关闭套接字
(newsockfd);
close(sockfd);
closereturn 0;
}
带外数据(Out-of-Band data)是在正常数据流之外传输的特殊数据。在 Linux C++ socket 编程中,可以使用 fcntl 函数和 sockatmark 函数来处理带外数据。
fcntl 函数可以用于获取或设置文件描述符的属性。对于套接字,可以使用 fcntl 函数来设置和检查带外数据的接收标志。
设置接收带外数据的标志:
int enableOOB = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_OOBINLINE, &enableOOB, sizeof(enableOOB)) < 0) {
("setsockopt failed");
perrorreturn -1;
}
上述代码将 SO_OOBINLINE 选项设置为 1,表示将带外数据与普通数据混合在一起接收。
检查是否接收到带外数据:
int flags = fcntl(sockfd, F_GETFL, 0);
if (flags < 0) {
("fcntl failed");
perrorreturn -1;
}
if (flags & OOB) {
("Received Out-of-Band data\n");
printf}
上述代码使用 fcntl 函数获取套接字的标志,并检查是否设置了 OOB 标志,如果设置了,则表示接收到了带外数据。
sockatmark 函数用于检查套接字的当前位置是否位于带外数据的边界(即下一次接收将接收到带外数据)。
int atMark = sockatmark(sockfd);
if (atMark < 0) {
("sockatmark failed");
perrorreturn -1;
}
if (atMark) {
("At Out-of-Band data mark\n");
printf}
上述代码使用 sockatmark 函数检查套接字的当前位置是否位于带外数据的边界,如果是,则表示当前位置是带外数据的边界。
recv(read)函数没有数据时会阻塞等待、套接字输出队列没有足够空间来发送消息时,send 函数会阻塞。
非阻塞模式下,这些函数不会阻塞而会失败,将 errno 置为 EWOULDBLOCK 或者 EAGAIN,可以通过 select、poll、epoll 判断是否能接收或传输数据。
套接字异步 I/O 管理命令,详细的可以问 chatgpt
(fd,F_SETOWN,pid)
fcntl(fd,FIOSETOWN,pid)
ioctl(fd,SIOCSPGRP,pid)
ioctl(fd,F_SETFL,flags|O_ASYNC)
fcntl(fd,FIOASYNC,&n) ioctl