linux下socket编程

大端与小端的区别;
什么是字节序;
为什么出现网络字节序。

1、字节序

字节序就是说一个对象的多个字节在内存中如何排序存放的;比如我们要想往一个地址a中写入一个整型数据0x12345678,那么最后在内存中是如何存放这4个字节的呢?
0x12这个字节值为最高有效字节,也就是整数值的最高位,0x78为最低有效字节。
大端字节序:高位地地址,节省空间;最高有效字节落在低地址上
小端字节序:最低有效字节落在低地址上的字节存放方式;
同样的字节序12 34 56 78在大端序机器中会识别为0x12345678;
小端序机器中识别为:0x78563421(0x12+0x3400+0x560000…=0x
78563421)。
Intel处理器大多数使用小端字节序;Motorola处理器大多数使用大端(big endian)字节序;arm即可设为大端,又可设为小端。如果直接在2个不同字节序的主机之间传输数据会引起混乱。

2、比特序

字节序是一个对象中的多个字节之间的顺序问题,比特序就是一个字节中的8个比特位(bit)之间的顺序问题;一般情况下系统的比特序和字节序一致的;
字节序是一个对象中的多个之间的顺序问题,比特序就是一个字节中的8个比特位(bit)之间的顺序问题;一般情况下系统的比特序和字节序保持一致的;一个字节由8个bit组成,这8个bit也存在如何排序的情况,跟字节序类似的有最高有效比特位、最低有效比特位。
比特序1 0 0 1 0 0 1 0在大端系统中最高有效比特位为1,最低有效比特位为0,字节的值为0x92;在小端系统中最高、最低有效比特序则相反为0、1,字节的值为0x49。

3、字节序转换函数ntohl(s)、htonl(s)

在socket编程中经常要用到网络字节序转换函数ntohl、htonl来进行主机序和网络序(大端序)的转换,在主机序为小端的系统中字节序列78 56 34 12经过htonl转换后字节序列变成12 34 56 78:

4、网络字节序

为了能让不同处理器架构的机器进行通信,他们都需要将本机上的字节序转换成网络字节序,这样就解决了不同处理器之间的矛盾。一般来说网络字节序和大端机器上的字节序是一样的;POSIX提供了4个函数(也可能是用宏来实现的),可以让本机字节序和网络字节序之间进行互转。
它们分别是:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//32位无符号数主机序转换网络字节序
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);//32位无符号数网络序转换主机序
uint16_t ntohs(uint16_t netshort);
其中函数名字中h表示host(本机),n表示network(网络),而 l 表示要转换的数据是4字节,s 表示要转换的数据是2字节。

#include <stdio.h>
#include <arpa/inet.h>

union INT
{
    char data[4];
    int x;
};
int main()
{
    int i;
    union INT a;
    union INT b;
    a.x = 0x10203040;
    b.x = htonl(a.x);//本机字节序转换为网络字节序
    printf ("本机字节序小端 a = 0x%08x\n",a.x);//08指定数据的最小输出位数为8
                              //,若不够8位,则补零,若大于8位,则按照原位数输出
    for (i = 0; i < 4; ++i)
    {
        printf ("[%p] : %02x\n",a.data+i,a.data[i]);
    }
    putchar('\n');
    printf ("网络字节序一般大端: b = 0x%08x\n",b.x);
    for (i = 0; i < 4; ++i)
    {
        printf ("[%p] : %02x\n",b.data+i,b.data[i]);
    }
    return 0;
}

本机字节序是小端的,网络字节序是大端需要通过函数htonl把本机地址转换成大端的。
IP地址
IPv4地址本质上是32位无符号整数;在Linux操作系统中,使用了in_addr_t类型来表示这样一个4字节的整数,它是由typedef定义的:
typedef unit32_t in_addr_t;
in_addr_t这个类型保存的数据,到底是按本机字节序保存的,还是网络字节序保存的,这是不确定的。因此Linux重新定义一个结构体来保存IP地址(网络字节序)
struct in_addr
{
in_addr_t s_addr;
}
如果说你传了一个本机字节序的 unsigned int 类型的整数给 in_addr 的 s_addr 成员,很抱歉,后面使用到该结构体的函数都会出错。
IP地址转换相关的函数
aton中a(address)表示点分十进制的地址,n表示网络字节序地址,ntoa类似;
函数lnaof表示local network address part of the Internet address in,即主机号;
函数netof表示network number part of the Internet address in,即网络号;
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 将点分十进制直接转换成 in_addr 类型,我们很喜欢这个函数
int inet_aton(const char *cp, struct in_addr *inp);
// 将点分十进制转换成 in_addr_t 类型,返回值保存的是网络字节序
in_addr_t inet_addr(const char *cp);
// 将点分十进制转换成 in_addr_t 类型,返回值保存的是本机字节序
in_addr_t inet_network(const char *cp);
// 将 in_addr 地址转换成点分十进制,注意这个函数不是线程安全的
char *inet_ntoa(struct in_addr in);
// 根据网络号和主机号制作 in_addr 类型地址
struct in_addr inet_makeaddr(int net, int host);
// 返回 ip 地址的主机号,返回的结果是本机字节序的
in_addr_t inet_lnaof(struct in_addr in);
// 返回 ip 地址的网络号,返回的结果是本机字节序的
in_addr_t inet_netof(struct in_addr in);

网络地址

#if 0

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);
// 将 cp 字符串表示的 IP 地址,转换为网络字节序的 IP 地址
// cp 可以是以下形式:
//    a.b.c.d  如:192.168.0.105
//    a.b.c    c 占 16 位
//    a.b      b 占 24 位
//    a        a 占 32 位
// 成功返回 1,失败返回 0

in_addr_t inet_addr(const char *cp);
// 将 cp 字符串表示的 IP 地址,转换为网络字节序的二进制形式
// 成功返回 IP 的网络字节序,失败返回 INADDR_NONE

in_addr_t inet_network(const char *cp);
// 将 cp 字符串表示的 IP 地址,转换为本机字节序的二进制形式
// 成功返回 IP 的本机字节序,失败返回 -1

char *inet_ntoa(struct in_addr in);
// 将 in 表示的网络字节序的 IP 地址转换为“点分十进制”形式的字符串
// 成功返回 IP 地址的字节串,失败返回 "255.255.255.255"
// 注意,返回的地址是一个静态局部缓冲区,后续的调用会覆盖之前的结果

// IPv4 地址
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 */
};
typedef uint32_t in_addr_t;
#endif

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char **argv)
{
    char *ip;
    struct in_addr addr;
    in_addr_t inaddr;
    if (argc != 2)
    {
        printf ("用法: %s <IP地址>\n",ip);
        return -1;
    }
    ip = argv[1];
    if (0 == inet_aton(ip,&addr))    //把有效的IP地址转换为网络字节序
    {
        printf ("%s 是无效的IP地址\n",ip);
    }
    else
    {
        printf ("%s 对应的网络字节序数值为: %#x\n",ip,addr.s_addr);
    }
    inaddr = inet_addr(ip);     //把有效的IP地址转换为网络字节序
    if (INADDR_NONE == inaddr)  //INADDR_NONE表示禁止的IP地址
    {
        printf ("%s 是非法IP地址\n",ip);
    }
    else
    {
        printf ("%s 对应的网络字节序数值为: %#x\n",ip,inaddr);
    }
    inaddr = inet_network(ip);    //把有效的IP地址转换为本机字节序
    if (-1 == inaddr)
    {
        printf("%s 是非法的IP地址\n",ip);
    }
    else
    {
        printf ("%s 对应的本机字节序数值为: %#x\n",ip,inaddr);
    }
    ip = inet_ntoa(addr);       //把网络字节序转换为IP地址(点分十进制)
    printf ("%#x 转换为IP地址: %s\n",addr.s_addr,ip);
    return 0;
}

解析域名

#if 0

头文件:#include <netdb.h>
       #include <sys/socket.h>
extern int h_errno;

函数原型:struct hostent *gethostbyname(const char *name);
// 将 name 转换为 struct hostent 指针指向的主机信息
// 成功返回非空指针(一个hostent的结构),失败返回 NULL
// 非空时,指针可能指向静态缓冲区

头文件:#include <sys/socket.h>       /* for AF_INET */
函数原型:struct hostent *gethostbyaddr(const void *addr,socklen_t len, int type);
//函数gethostbyaddr取一个二进制的IP地址并试图找到相应于此地址的主机名与gethostbynme相反
void sethostent(int stayopen);

void endhostent(void);

void herror(const char *s);
// 将 s 后加 ": " 再加 h_errno 表示的错误信息字符串一起输出
const char *hstrerror(int err);
// 将 err 表示的错误信息转换为字符串
/* System V/POSIX extension */
struct hostent *gethostent(void);

/* GNU extensions */
struct hostent *gethostbyname2(const char *name, int af);

int gethostent_r(
            struct hostent *ret, char *buf, size_t buflen,
            struct hostent **result, int *h_errnop);

int gethostbyaddr_r(const void *addr, socklen_t len, int type,
            struct hostent *ret, char *buf, size_t buflen,
            struct hostent **result, int *h_errnop);

int gethostbyname_r(const char *name,
            struct hostent *ret, char *buf, size_t buflen,
            struct hostent **result, int *h_errnop);

int gethostbyname2_r(const char *name, int af,
            struct hostent *ret, char *buf, size_t buflen,
            struct hostent **result, int *h_errnop);

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 */
}
#define h_addr h_addr_list[0] /* for backward compatibility */

// h_addr_list 数组并不是字符串指针数组,而是地址的网络字节序的
// 二进制数据的指针数组

#endif

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
extern int h_errno;
int main(int argc,char *argv[])
{
    struct hostent *hostent;
    char *hostname;
    if (argc != 2)
    {
        printf ("用法: %s <域名>\n",argv[0]);
        return -1;
    }
    hostname = argv[1];
    hostent = gethostbyname(hostname);    //用域名或主机名获取IP地址
    if (NULL == hostent)
    {
        herror("gethostbyname()失败");
        printf ("gethostbyname()失败:%s\n",hstrerror(h_errno));
        return -1;
    }
                        //解析主机信息
    printf ("主机官方名称: %s\n",hostent->h_name);    //主机的规范名
    for (int i = 0; hostent->h_aliases[i];i++)
    {
        printf ("[%d] - %s\n",i,hostent->h_aliases[i]);    //主机的别名
    }
    if (hostent->h_addrtype == AF_INET)    //主机IP地址的类型
    {
        printf ("地址类型是 AF_INET\n");
    }
    else
    {
        printf ("地址类型不是 AF_INET\n");
    }
    printf ("地址长度:%d\n",hostent->h_length);    //主机的IP地址长度
    for (int i = 0; hostent->h_addr_list[i];i++)    //获取主机的IP地址
    {
        char *ip = inet_ntoa(*((struct in_addr *)hostent->h_addr_list[i]));
        printf ("[%d]:%s\n0",i,ip);
    }
    char *ip = inet_ntoa(*((struct in_addr *)hostent->h_addr));
    printf ("[0]:%s\n",ip);
    return 0;
}

一、套接字socket

安装socket直接使用socket函数即可
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
【1】应用头文件: #include <sys/socket.h>
【2】函数标准式:int socket(int domain, int type, int protocol);
【3】功能与作用:创建通信的端口
【4】函数参数 :domain告诉操作系统要用哪家写的网络协议,比如AF_INET(IPv4协议),AF_INET6(IPv6协议);
type字段表示通信的交流方式,有以下选项:
SOCK_STREAM:有序的,可靠的,双向的基于连接的字节流,
SOCK_DGRAM:无连接的,不可靠的,固定长度的报文,
SOCK_RAM:原始套接字。
protocol:用哪家的网络协议确定了,用哪种通信方式确定了,最后使用哪种具体的协议来实现这种通信方式;通常设置为0,表示只用一种默认的方式。SOCK_STREAM默认使用的协议是TCP,SOCK_DGRAM默认使用的协议是UDP。
【5】返回值 :成功返回套接字的描述符,失败返回-1;

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
int main()
{
    int sockfd,sockfd1,sockfd2;
    sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if (-1 == sockfd)
    {
        printf ("创建套接字失败");
        return -1;
    }
    printf ("创建套接字成功,套接字为 %d \n",sockfd);
    sockfd1 = socket(AF_INET,SOCK_STREAM,0);
    if (-1 == sockfd1)
    {
        printf ("创建套接字失败");
        return -1;
    }
    printf ("创建套接字成功,套接字为 %d \n",sockfd1);
    /*sockfd2 = socket(AF_INET,SOCK_RAM,IPPROTO_TCP);    //原始套接字比较特殊
    if (-1 == sockfd2)
    {
        printf ("创建套接字失败");
        return -1;
    }
    printf ("创建套接字成功,套接字为 %d \n",sockfd2);*/
    return 0;
}

二、将socket与套接字地址绑定

在网络编程中,把(IP地址+端口号)这样的一对值称呼为套接字地址;在计算机中,通常使用192.168.166.5:80 这种ip:port 的形式来表示套接字地址。
socket地址有多种:sockaddr是通用地址结构;sockaddr_in是IPv4地址结构;sockaddr_un是Unix地址结构。

套接字地址在程序中是一个结构体
通用地址结构体:
struct sockaddr
{
  unsigned short sa_family; /* 地址族, AF_xxx /
  char sa_data[14]; /
14 字节的协议地址 */
};

IPv4结构体:
struct sockaddr_in {
sa_family_t sin_family; /* 这个值固定写 AF_INET /
in_port_t sin_port; /
网络字节序的端口号 /
struct in_addr sin_addr; /
in_addr 类型的 ip 地址 */
};
函数bind可以将套接字地址与socket进行绑定
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
这上面的套接字地址的类型是 struct sockaddr 而不是我们前面学过的 struct sockaddr_in 呢?你的疑问是正常的,因为 bind 函数不仅仅是设计给 IPv4 用的,也会给 IPv6 地址,所以 struct sockaddr 就有点像一个抽象类。
因此,你在构造完套接字地址后,直接把 struct sockaddr_in 的地址强转成 struct sockaddr 指针就行了。
参数 addrlen 是参数 addr 指向的套接字地址的结构体大小
还需要注意的地方:如果你并不想让别的进程连接到你的进程,你并不需要将 socket 绑定到套接字地址上。需要绑定套接字地址的进程,通常就是可以让别人找到你并连接上你,所以这种进程通常都称之为服务器进程。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int sockfd;    //套接字描述符变量
    struct sockaddr_in svr_addr,clt_addr;    //服务器地址与客户端地址变量
    in_port_t port;    //服务器端口
    in_addr_t inaddr;    //网络字节序保存
    char *ip ;
    socklen_t socklen = sizeof svr_addr;
    char recv_buf[32] = "";
    char send_buf[32] = "hello server";
    if (argc != 3)
    {
        printf ("用法:%s <本机IP> <服务器端口号>\n",argv[0]);
        return -1;
    }
    ip = argv[1];
    port = atoi(argv[2]);
    sockfd = socket(AF_INET,SOCK_DGRAM,0);    //创建套接字失败返回-1
    if (-1 == sockfd)
    {
        perror("创建套接字失败");
        return -1;
    }
    inaddr = inet_addr(ip);    //本机地址转换为网络字节序的二进制形式
    if (INADDR_NONE == inaddr)
    {
        printf ("%s 是非法IP地址\n",ip);
        goto _out;
    }
    svr_addr.sin_family = AF_INET;  //设置IPv4类型
    svr_addr.sin_port = htons(port);    //设置网络端口号
    svr_addr.sin_addr.s_addr = inaddr;    //IP地址

    if (-1 == sendto(sockfd,send_buf,strlen(send_buf),0,
                (struct sockaddr *)&svr_addr,socklen))
    {
        perror("发送失败");
        goto _out;
    }
    printf ("发送成功\n");

    if (-1 == recvfrom(sockfd,recv_buf,strlen(recv_buf),0,
                (struct sockaddr *)&clt_addr,&socklen))
    {
        perror("接收失败");
        goto _out;
    }
    printf ("收到 %s(%d)发来的消息:%s\n",
            inet_ntoa(clt_addr.sin_addr),
            ntohs(clt_addr.sin_port),recv_buf);
_out:
    close(sockfd);    //关闭套接字
    return 0;
}

UDP编程流程:

服务器

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#if 0
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// 创建用于通信的端口,返回描述符

// domain 包含以下域:
// Name                Purpose                          Man page
// AF_UNIX, AF_LOCAL   Local communication              unix(7)
// AF_INET             IPv4 Internet protocols          ip(7)
// AF_INET6            IPv6 Internet protocols          ipv6(7)
// AF_IPX              IPX - Novell protocols
// AF_NETLINK          Kernel user interface device     netlink(7)
// AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
// AF_AX25             Amateur radio AX.25 protocol
// AF_ATMPVC           Access to raw ATM PVCs
// AF_APPLETALK        AppleTalk                        ddp(7)
// AF_PACKET           Low level packet interface       packet(7)
// AF_ALG              Interface to kernel crypto API

// type 有以下选项:
// SOCK_STREAM 基于流的,适用于 TCP 套接字
// SOCK_DGRAM  基于数据报的,适用于 UDP 套接字
// SOCK_RAW    原始套接字

// protocol 子协议,如果没有子协议则为 0

// 成功返回套接字的描述符,失败返回 -1


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
// 绑定长度为 addrlen 的 addr 地址到 sockfd 套接字
// sockfd  套接字描述符
// addr    通用地址指针,传递具体地址时要进行强制类型转换
// addrlen 地址长度
// 成功返回 0,失败返回 -1

// 通用地址结构体
struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
};


#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 只能用于 TCP
// 从 sockfd 套接字接收网络信息,保存到大小为 len 的缓冲区 buf 中
// flags 可取 0 或以下值按位或:
//     MSG_DONTWAIT  非阻塞
//     MSG_OOB       带外数据(紧急数据)
//     MSG_PEEK      只提取数据而不从接收队列中删除它
//     MSG_WAITALL   阻塞直到所有请求都满足
// recv() 等价于以下调用:
//     recvfrom(fd, buf, len, flags, NULL, 0));

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
            struct sockaddr *src_addr, socklen_t *addrlen);
// 用于 UDP,但也可以用于 TCP
// 从 sockfd 套接字接收网络信息,保存到大小为 len 的缓冲区 buf 中
// 保存发送者的地址信息到大小为 addrlen 的地址 src_addr 中
// 注意,尽管 addrlen 是用来存放发送者的地址的大小的,但在有的系统
// 中必须提供一个正确的大小,才能正确接收网络数据

// 以上函数成功时返回收到的字节数,失败时返回 -1
// 返回 0 的情况:
// 1. 基于流的套接字(TCP),发送方已关闭,接收方会收到 0 个字节,
//    这相当于"文件尾"
// 2. 基于数据报的套接字(UDP)允许长度为 0 的数据报,当收到这样的
//    数据报,返回 0 个字节
// 3. 从流套接字收到 0 个字节时返回 0 字节


#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 只用于 TCP
// 将数据大小为 len 的 buf 中的数据通过 sockfd 发送
// flags 取值为 0 或以参下参数按位或:
//     MSG_DONTWAIT  非阻塞
//     MSG_NOSIGNAL  向已关闭的流套接字写不产生 SIGPIPE 信号
//     MSG_OOB       带外数据(紧急数据)

// send() 等价于以下调用:
//     sendto(sockfd, buf, len, flags, NULL, 0);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
            const struct sockaddr *dest_addr, socklen_t addrlen);
// 既可以用于 UDP 也可以用于 TCP
// 将数据大小为 len 的 buf 中的数据通过 sockfd 发送到大小为 addrlen
// 的地址 dest_addr
// send() 和 sendto() 返回成功发送的字节数,或失败返回 -1


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
功能说明:
获取或者设置与某个套接字关联的选 项。选项可能存在于多层协议中,它们总会出现在最上面的套接字层。当操作套接字选项时,选项位于的层和选项的名称必须给出。为了操作套接字层的选项,应该 将层的值指定为SOL_SOCKET。为了操作其它层的选项,控制选项的合适协议号必须给出。例如,为了表示一个选项由TCP协议解析,层应该设定为协议 号TCP。
int getsockopt(int sockfd, int level, int optname,
            void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
            const void *optval, socklen_t optlen);
// 获取/设置套接字选项
// level 可取以下值:
//     SOL_SOCKET     基本套接口  
//     IPPROTO_IP     IPv4套接口  
//     IPPROTO_IPV6   IPv6套接口  
//     IPPROTO_TCP    TCP套接口  
// optname 可取以下值:
//     SO_BROADCAST 广播
//     SO_RCVBUF    接收缓冲区
//     SO_REUSEADDR 地址可重用
//     SO_REUSEPORT 端口可重用
//     SO_SNDBUF    发送缓冲区
// 成功返回 0,失败返回 -1,errno被设为以下的某个值  
EBADF:sock不是有效的文件描述词
EFAULT:optval指向的内存并非有效的进程空间
EINVAL:在调用setsockopt()时,optlen无效
ENOPROTOOPT:指定的协议层不能识别选项
ENOTSOCK:sock描述的不是套接字

#endif

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in svr_addr, clt_addr;
    in_port_t port;
    in_addr_t inaddr;
    char *ip = "192.168.177.151";
    int optval = 1;              // 非 0 使能地址可重用
    socklen_t socklen = sizeof clt_addr, optlen = sizeof optval;
    char recv_buf[32] = "";
    char send_buf[32] = "Hello client";

    if (argc != 3)            // 检查参数个数
    {
        printf("用法:%s <本机 IP> <端口号>\n", argv[0]);
        return -1;
    }
    ip = argv[1];
    port = atoi(argv[2]);

    // 创建 UDP 套接字
    sockfd = socket(AF_INET /*PF_INET*/, SOCK_DGRAM, 0);
    if (-1 == sockfd)
    {
        perror("创建套接字失败");
        return -1;
    }

    // 设置套接字选项 - 地址可重用
    if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
                    &optval, optlen))
    {
        perror("设置地址可重用失败");
        goto _out;
    }

    // 绑定本机地址
    inaddr = inet_addr(ip);
    if (INADDR_NONE == inaddr)
    {
        printf("%s 是非法 IP 地址\n", ip);
        goto _out;
    }
    svr_addr.sin_family = AF_INET;   // PF_INET
    svr_addr.sin_port = htons(port); // 端口必须转换为网络字节序
    //svr_addr.sin_addr.s_addr = inaddr;   // 绑定到本机的某一个 IP
    // 绑定到本机任意网络接口
    svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);  
    if (-1 == bind(sockfd, (struct sockaddr *) &svr_addr, sizeof svr_addr))
    {
        perror("绑定失败");
        goto _out;
    }

    // 收
    if (-1 == recvfrom(sockfd, recv_buf, sizeof recv_buf, 0,
                    (struct sockaddr *) &clt_addr, &socklen))
    {
        perror("接收失败");
        goto _out;
    }
    printf("收到 %s(%d) 发来的消息:%s\n",
                inet_ntoa(clt_addr.sin_addr),
                ntohs(clt_addr.sin_port), recv_buf);

    // 发
    if (-1 == sendto(sockfd, send_buf, strlen(send_buf), 0,
                    (struct sockaddr *) &clt_addr, socklen))
    {
        perror("发送失败");
        goto _out;
    }
    printf("发送成功\n");

_out:
    // 关闭套接字
    close(sockfd);

    return 0;
}

客户端

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#if 0

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
// 创建用于通信的端口,返回描述符

// domain 包含以下域:
// Name                Purpose                          Man page
// AF_UNIX, AF_LOCAL   Local communication              unix(7)
// AF_INET             IPv4 Internet protocols          ip(7)
// AF_INET6            IPv6 Internet protocols          ipv6(7)
// AF_IPX              IPX - Novell protocols
// AF_NETLINK          Kernel user interface device     netlink(7)
// AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
// AF_AX25             Amateur radio AX.25 protocol
// AF_ATMPVC           Access to raw ATM PVCs
// AF_APPLETALK        AppleTalk                        ddp(7)
// AF_PACKET           Low level packet interface       packet(7)
// AF_ALG              Interface to kernel crypto API

// type 有以下选项:
// SOCK_STREAM 基于流的,适用于 TCP 套接字
// SOCK_DGRAM  基于数据报的,适用于 UDP 套接字
// SOCK_RAW    原始套接字

// protocol 子协议,如果没有子协议则为 0

// 成功返回套接字的描述符,失败返回 -1


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
// 绑定长度为 addrlen 的 addr 地址到 sockfd 套接字
// sockfd  套接字描述符
// addr    通用地址指针,传递具体地址时要进行强制类型转换
// addrlen 地址长度
// 成功返回 0,失败返回 -1

// 通用地址结构体
struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
};


#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 只能用于 TCP
// 从 sockfd 套接字接收网络信息,保存到大小为 len 的缓冲区 buf 中
// flags 可取 0 或以下值按位或:
//     MSG_DONTWAIT  非阻塞
//     MSG_OOB       带外数据(紧急数据)
//     MSG_PEEK      只提取数据而不从接收队列中删除它
//     MSG_WAITALL   阻塞直到所有请求都满足
// recv() 等价于以下调用:
//     recvfrom(fd, buf, len, flags, NULL, 0));

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
            struct sockaddr *src_addr, socklen_t *addrlen);
// 用于 UDP,但也可以用于 TCP
// 从 sockfd 套接字接收网络信息,保存到大小为 len 的缓冲区 buf 中
// 保存发送者的地址信息到大小为 addrlen 的地址 src_addr 中
// 注意,尽管 addrlen 是用来存放发送者的地址的大小的,但在有的系统
// 中必须提供一个正确的大小,才能正确接收网络数据

// 以上函数成功时返回收到的字节数,失败时返回 -1
// 返回 0 的情况:
// 1. 基于流的套接字(TCP),发送方已关闭,接收方会收到 0 个字节,
//    这相当于"文件尾"
// 2. 基于数据报的套接字(UDP)允许长度为 0 的数据报,当收到这样的
//    数据报,返回 0 个字节
// 3. 从流套接字收到 0 个字节时返回 0 字节


#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 只用于 TCP
// 将数据大小为 len 的 buf 中的数据通过 sockfd 发送
// flags 取值为 0 或以参下参数按位或:
//     MSG_DONTWAIT  非阻塞
//     MSG_NOSIGNAL  向已关闭的流套接字写不产生 SIGPIPE 信号
//     MSG_OOB       带外数据(紧急数据)

// send() 等价于以下调用:
//     sendto(sockfd, buf, len, flags, NULL, 0);

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
            const struct sockaddr *dest_addr, socklen_t addrlen);
// 既可以用于 UDP 也可以用于 TCP
// 将数据大小为 len 的 buf 中的数据通过 sockfd 发送到大小为 addrlen
// 的地址 dest_addr
// send() 和 sendto() 返回成功发送的字节数,或失败返回 -1

#endif

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in svr_addr, clt_addr;
    in_port_t port;
    in_addr_t inaddr;
    socklen_t socklen = sizeof svr_addr;
    char *ip;
    char recv_buf[32] = "";
    char send_buf[32] = "Hello server";

    // 检查参数个数
    if (argc != 3)
    {
        printf("用法:%s <服务器 IP> <服务器端口号>\n", argv[0]);
        return -1;
    }
    ip = argv[1];
    port = atoi(argv[2]);

    // 创建 UDP 套接字
    sockfd = socket(AF_INET /*PF_INET*/, SOCK_DGRAM, 0);
    if (-1 == sockfd)
    {
        perror("创建套接字失败");
        return -1;
    }

    // 客户端不必绑定本机地址

    // 配置服务器地址
    inaddr = inet_addr(ip);
    if (INADDR_NONE == inaddr)
    {
        printf("%s 是非法 IP\n", ip);
        goto _out;
    }
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_port = htons(port);
    svr_addr.sin_addr.s_addr = inaddr;
    
    // 发
    if (-1 == sendto(sockfd, send_buf, strlen(send_buf), 0,
                    (struct sockaddr *) &svr_addr, socklen))
    {
        perror("发送失败");
        goto _out;
    }
    printf("发送成功\n");

    // 收
    if (-1 == recvfrom(sockfd, recv_buf, sizeof recv_buf, 0,
                    (struct sockaddr *) &clt_addr, &socklen))
    {
        perror("接收失败");
        goto _out;
    }
    printf("收到 %s(%d) 发来的消息:%s\n",
                inet_ntoa(clt_addr.sin_addr),
                ntohs(clt_addr.sin_port), recv_buf);

_out:
    // 关闭套接字
    close(sockfd);

    return 0;
}

TCP编程流程:

服务器:

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#if 0

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);
// 在套接字 sockfd 上设置监听队列长度为 backlog
// backlog 在套接字 sockfd 上最多等待连接的队列长度
// 如果队列已满,而此时有连接请求,则客户端会收到
// ECONNREFUSED 错误
// 成功返回 0,失败返回 -1


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 在套接字 sockfd 上接收客户端连接,返回连接描述符
// addr    保存连接上的客户端的地址
// addrlen 地址的大小。必须初始化为 addr 的大小
// 成功返回非负的 连接描述符,失败返回 -1
// 与客户端的收发是基于连接描述符


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
// 向地址为 addr 的主机发起连接请求
// sockfd 可以是 SOCK_STREM 类型,只能成功连接一次;
// 也可以是 SOCK_DGRAM 类型,可以多次发起连接,
// 每次可以分配不同的地址
// 成功返回 0,失败返回 -1

#endif

int main(int argc, char **argv)
{
    in_port_t port;
    int sockfd, connfd;
    int optval = 1;
    struct sockaddr_in svr_addr;   // 服务器地址
    int backlog = 5;
    struct sockaddr_in clt_addr;   // 客户端地址
    socklen_t addrlen = sizeof clt_addr;
    char rbuf[32] = "", wbuf[32] = "Hello client";

    if (argc != 3)
    {
        printf("用法:%s <端口号> <监听队列长度>\n", argv[0]);
        return -1;
    }
    port = atoi(argv[1]);
    backlog = atoi(argv[2]);

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("创建套接字失败");
        return -1;
    }

    // 设置地址可重用
    if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
                    &optval, sizeof optval))
    {
        perror("设置地址可重用失败");
        goto _out;
    }

    // 绑定服务器地址
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_port = htons(port);
    svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    //svr_addr.sin_addr.s_addr = htonl(inet_addr("192.168.0.121"));
    if (-1 == bind(sockfd, (struct sockaddr *) &svr_addr,
                    sizeof svr_addr))
    {
        perror("绑定失败");
        goto _out;
    }

    // 设置监听队列长度
    if (-1 == listen(sockfd, backlog))
    {
        perror("设置监听队列失败");
        goto _out;
    }

    // 接收 5 次客户端连接
    for (int i = 0; i < 5; i++)
    {
        // 接收客户端连接
        connfd = accept(sockfd, (struct sockaddr *) &clt_addr, &addrlen);
        if (-1 == connfd)
        {
            perror("接收客户端连接失败");
            goto _out;
        }
        printf("第 %d 个客户端 %s(%d) 连接成功\n", i + 1,
                    inet_ntoa(clt_addr.sin_addr),
                    ntohs(clt_addr.sin_port));

        // 收
        if (-1 == read(connfd, rbuf, sizeof rbuf))
        {
            perror("接收失败");
            goto _out2;
        }
        printf("收到客户端信息:%s\n", rbuf);

        // 发
        if (-1 == write(connfd, wbuf, strlen(wbuf)))
        {
            perror("发送失败");
            goto _out2;
        }
        printf("发送成功\n");

_out2:
        // 关闭连接
        close(connfd);
    }

_out:
    // 关闭套接字
    close(sockfd);

    return 0;
}

客户端

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#if 0

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);
// 在套接字 sockfd 上设置监听队列长度为 backlog
// backlog 在套接字 sockfd 上最多等待连接的队列长度
// 如果队列已满,而此时有连接请求,则客户端会收到
// ECONNREFUSED 错误
// 成功返回 0,失败返回 -1


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 在套接字 sockfd 上接收客户端连接,返回连接描述符
// addr    保存连接上的客户端的地址
// addrlen 地址的大小。必须初始化为 addr 的大小
// 成功返回非负的 连接描述符,失败返回 -1
// 与客户端的收发是基于连接描述符


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
// 向地址为 addr 的主机发起连接请求
// sockfd 可以是 SOCK_STREM 类型,只能成功连接一次;
// 也可以是 SOCK_DGRAM 类型,可以多次发起连接,
// 每次可以分配不同的地址
// 成功返回 0,失败返回 -1

#endif

int main(int argc, char **argv)
{
    in_port_t port;
    int sockfd;
    struct sockaddr_in svr_addr;   // 服务器地址
    char rbuf[32] = "", wbuf[32] = "Hello server";
    char *ip;
    in_addr_t inaddr;

    if (argc != 3)
    {
        printf("用法:%s <服务器 IP> <端口号>\n", argv[0]);
        return -1;
    }
    ip = argv[1];
    port = atoi(argv[2]);

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("创建套接字失败");
        return -1;
    }

    // 发起向服务器的连接
    inaddr = inet_addr(ip);
    if (INADDR_NONE == inaddr)
    {
        printf("%s 是非法地址\n", ip);
        goto _out;
    }
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_port = htons(port);
    svr_addr.sin_addr.s_addr = inaddr;
    if (-1 == connect(sockfd, (struct sockaddr *) &svr_addr,
                    sizeof svr_addr))
    {
        perror("连接服务器失败");
        goto _out;
    }
    printf("连接服务器成功\n");
    
    // 发
    if (-1 == write(sockfd, wbuf, strlen(wbuf)))
    {
        perror("发送失败");
        goto _out;
    }
    printf("发送成功\n");

    // 收
    if (-1 == read(sockfd, rbuf, sizeof rbuf))
    {
        perror("接收失败");
        goto _out;
    }
    printf("收到服务器信息:%s\n", rbuf);

_out:
    // 关闭套接字
    close(sockfd);

    return 0;
}

设置忽略SIGPIPE信号
服务器

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <errno.h>

#if 0

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);
// 在套接字 sockfd 上设置监听队列长度为 backlog
// backlog 在套接字 sockfd 上最多等待连接的队列长度
// 如果队列已满,而此时有连接请求,则客户端会收到
// ECONNREFUSED 错误
// 成功返回 0,失败返回 -1


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 在套接字 sockfd 上接收客户端连接,返回连接描述符
// addr    保存连接上的客户端的地址
// addrlen 地址的大小。必须初始化为 addr 的大小
// 成功返回非负的 连接描述符,失败返回 -1
// 与客户端的收发是基于连接描述符


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
// 向地址为 addr 的主机发起连接请求
// sockfd 可以是 SOCK_STREM 类型,只能成功连接一次;
// 也可以是 SOCK_DGRAM 类型,可以多次发起连接,
// 每次可以分配不同的地址
// 成功返回 0,失败返回 -1

#endif

void handle_sigpipe(int signum)
{
    printf("收到 SIGPIPE 信号\n");
}

int main(int argc, char **argv)
{
    in_port_t port;
    int sockfd, connfd;
    int optval = 1;
    struct sockaddr_in svr_addr;   // 服务器地址
    int backlog = 5;
    struct sockaddr_in clt_addr;   // 客户端地址
    socklen_t addrlen = sizeof clt_addr;
    char rbuf[32] = "", wbuf[32] = "Hello client";

    if (argc != 3)
    {
        printf("用法:%s <端口号> <监听队列长度>\n", argv[0]);
        return -1;
    }
    port = atoi(argv[1]);
    backlog = atoi(argv[2]);

    // 避免 SIGPIPE 信号
    //signal(SIGPIPE, SIG_IGN);       // 忽略 SIGPIPE 信号
    signal(SIGPIPE, handle_sigpipe);  // 处理 SIGPIPE 信号

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("创建套接字失败");
        return -1;
    }

    // 设置地址可重用
    if (-1 == setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
                    &optval, sizeof optval))
    {
        perror("设置地址可重用失败");
        goto _out;
    }

    // 绑定服务器地址
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_port = htons(port);
    svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    //svr_addr.sin_addr.s_addr = htonl(inet_addr("192.168.0.121"));
    if (-1 == bind(sockfd, (struct sockaddr *) &svr_addr,
                    sizeof svr_addr))
    {
        perror("绑定失败");
        goto _out;
    }

    // 设置监听队列长度
    if (-1 == listen(sockfd, backlog))
    {
        perror("设置监听队列失败");
        goto _out;
    }

    // 接收 5 次客户端连接
    for (int i = 0; i < 5; i++)
    {
        // 接收客户端连接
        connfd = accept(sockfd, (struct sockaddr *) &clt_addr, &addrlen);
        if (-1 == connfd)
        {
            perror("接收客户端连接失败");
            goto _out;
        }
        printf("第 %d 个客户端 %s(%d) 连接成功\n", i + 1,
                    inet_ntoa(clt_addr.sin_addr),
                    ntohs(clt_addr.sin_port));

        // 收
        if (-1 == read(connfd, rbuf, sizeof rbuf))
        {
            perror("接收失败");
            goto _out2;
        }
        printf("收到客户端信息:%s\n", rbuf);

        for (int j = 0; j < 10; j++)
        {
            sleep(1);

            // 发
            if (-1 == write(connfd, wbuf, strlen(wbuf)))
            {
                perror("发送失败");
                if (EPIPE == errno)
                {
                    printf("客户端已关闭\n");
                }
                goto _out2;
            }
            printf("发送成功\n");
        }
_out2:
        // 关闭连接
        close(connfd);
    }

_out:
    // 关闭套接字
    close(sockfd);

    return 0;
}

客户端

#include <stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#if 0

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);
// 在套接字 sockfd 上设置监听队列长度为 backlog
// backlog 在套接字 sockfd 上最多等待连接的队列长度
// 如果队列已满,而此时有连接请求,则客户端会收到
// ECONNREFUSED 错误
// 成功返回 0,失败返回 -1


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 在套接字 sockfd 上接收客户端连接,返回连接描述符
// addr    保存连接上的客户端的地址
// addrlen 地址的大小。必须初始化为 addr 的大小
// 成功返回非负的 连接描述符,失败返回 -1
// 与客户端的收发是基于连接描述符


#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr,
            socklen_t addrlen);
// 向地址为 addr 的主机发起连接请求
// sockfd 可以是 SOCK_STREM 类型,只能成功连接一次;
// 也可以是 SOCK_DGRAM 类型,可以多次发起连接,
// 每次可以分配不同的地址
// 成功返回 0,失败返回 -1

#endif

int main(int argc, char **argv)
{
    in_port_t port;
    int sockfd;
    struct sockaddr_in svr_addr;   // 服务器地址
    char rbuf[32] = "", wbuf[32] = "Hello server";
    char *ip;
    in_addr_t inaddr;

    if (argc != 3)
    {
        printf("用法:%s <服务器 IP> <端口号>\n", argv[0]);
        return -1;
    }
    ip = argv[1];
    port = atoi(argv[2]);

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        perror("创建套接字失败");
        return -1;
    }

    // 发起向服务器的连接
    inaddr = inet_addr(ip);
    if (INADDR_NONE == inaddr)
    {
        printf("%s 是非法地址\n", ip);
        goto _out;
    }
    svr_addr.sin_family = AF_INET;
    svr_addr.sin_port = htons(port);
    svr_addr.sin_addr.s_addr = inaddr;
    if (-1 == connect(sockfd, (struct sockaddr *) &svr_addr,
                    sizeof svr_addr))
    {
        perror("连接服务器失败");
        goto _out;
    }
    printf("连接服务器成功\n");
    
    // 发
    if (-1 == write(sockfd, wbuf, strlen(wbuf)))
    {
        perror("发送失败");
        goto _out;
    }
    printf("发送成功\n");
/*
    // 收
    if (-1 == read(sockfd, rbuf, sizeof rbuf))
    {
        perror("接收失败");
        goto _out;
    }
    printf("收到服务器信息:%s\n", rbuf);
*/
_out:
    // 关闭套接字
    close(sockfd);

    return 0;
}

对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出.
为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数:signal(SIGPIPE, SIG_IGN);
这样, 第二次调用write方法时, 会返回-1, 同时errno置为EPIPE. 程序便能知道对端已经关闭.
在linux下写socket的程序的时候,如果尝试send到一个disconnected socket上,就会让底层抛出一个SIGPIPE信号。
这个信号的缺省处理方法是退出进程,大多数时候这都不是我们期望的。因此我们需要重载这个信号的处理方法。调用以下代码,即可安全的屏蔽SIGPIPE:signal (SIGPIPE, SIG_IGN);
client端通过pipe发送信号到server端后,就关闭client端,这时server端返回信息给client端时就会产生Broken pipe信号了,服务其就会被系统结束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值