Linux网络编程(一)

原文地址:http://blog.csdn.net/mayuan1210/article/details/73611345

网络编程复习基础知识,socket常用函数。

网络编程基础及基本socket函数

网络编程模型

CS模型

网络应用的标准模型是C/S模型,是非对称模型。如下图:

其服务器模型分为两种:

  • 循环服务器
    • 任一时刻只处理一个客户机请求,处理请求过程中下一请求等待
    • 节省服务器资源,响应时间长,适合处理非耗时请求
  • 并发服务器
    • 并发执行,每收到一个连接请求创建一个进程处理该连接,服务器继续等待下一连接
    • 响应速度快,占用系统资源多

编程模型图。。

BS模型

  • 三层/多层结构
  • 客户端由浏览器代替
  • 服务器端包括Web服务器、数据库服务器等
  • 主要采用HTTP协议
  • 主要用于WEB应用程序

网络协议

TCP

  • 面向连接,数据包可靠有序到达
  • 具有流量控制能力
  • 全双工通道

三次握手示意:

四次挥手:

UDP

非连接的,不可靠。

ICMP

通常由路由器产生,用来报告网络传输错误。封装在ip数据包中。

基本socket函数

基本流程可以有以下7步:

  1. 创建网络端点-socket
  2. 连接服务器-connect
  3. 绑定服务器地址和端口-bind
  4. 监听端口-listen
  5. 接受客户端连接-accept
  6. 关闭socket-close
  7. 接收和发送数据-read、write

套接字数据结构

主要用到以下三个地址结构:

通用套接字结构,其可以在不同协议族之间进行强制转换

struct sockaddr
{
  sa_family_t sa_faimly;//协议族(ushort 16字节)
  char sa_dara[14]; //数据
};
  
  
  • 1
  • 2
  • 3
  • 4
  • 5

以太网中一般使用如下结构

struct sockaddr_in
{
   u8 sin_len;//结构的长度,为16
   u8 sin_family;//协议族,通常AF_INET
  u16 sin_port;//16位端口号,网络字节序
  struct in_addr sin_addr;//ip地址32位
  char sin_zero[8];//未用
};
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

u8: unsigned char
u16:unsigned short int
u32:unsigned int

以上两个结构大小相等,只是对应关系不同而已。

其中,in_addr结构如下

struct in_addr
{
  u32 s_addr;//32位ip,网络字节序
};
  
  
  • 1
  • 2
  • 3
  • 4

创建socket

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

int socket(int domain, int type, int protocol);
/*domain:常用AF_INET即IPv4 Internet protocols
type:  SOCK_STREAM     SOCK_DGRAM  SOCK_RAW
protocol: 指定将要用到的具体的协议,一般对于给定的协议族、套接字类型,只有一种具体协议可用,所以一般设置为0
返回套接字描述符
*/
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

后面的所有函数几乎都是返回-1表示失败,不特别说明就是如此!

转化

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);//字符串转网络地址
char *inet_ntoa(struct in_addr in);//转为字符串
  
  
  • 1
  • 2
  • 3
  • 4
  • 5

网络字节序统一大端模式(高位在前,低位在后)

#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);

//在stdint.h中
typedef unsigned short int  uint16_t;
typedef unsigned char       uint8_t;
typedef unsigned int        uint32_t;
typedef unsigned long int   uint64_t;
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

connect

#include <sys/types.h>         
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  
  
  • 1
  • 2
  • 3

这些函数和python中几乎一样(谁让python只是包装了它呢),对于TCp协议来说,就是向服务器发起连接,第三个参数是addr的大小,成功返回0,只能连接一次。对于UDP而言,可以连接多次,这里的”连接”的意义可就不是TCP的那个了,只是设置addr为默认发送地址,并且,从其它地址发来的udp数据包都丢弃!默认地址是可以重新设置的,没关系。

bind

#include <sys/types.h>       
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  
  
  • 1
  • 2
  • 3

简单说就是把地址和套接字描述符关联起来即把发送数据的端口地址和IP地址进行了指定。发送数据时如果不指定,就临时选择一个端口。

对于TCp的服务器而言,显然只有bind之后才能接受连接(不知道地址怎么连?用脚啊)。客户端就不需要了,不绑定地址时系统自动分配一个端口,并用该端口和本机ip地址填充客户端socket地址。

listen

#include <sys/types.h>          
#include <sys/socket.h>
int listen(int sockfd, int backlog);
  
  
  • 1
  • 2
  • 3

将socket转为被动socket,等待服务器的连接。第二个参数是队列长度,即最大支持连接数。

accept

#include <sys/types.h>        
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  
  
  • 1
  • 2
  • 3

接受客户端的连接,适用于TCP,它提取请求队列(见上一个方法)的第一个请求,然后创建一个新的套接字描述符用于传输数据,新的套接字不再是监听状态。原来的套接字不受影响。

accept返回时,会将客户端的地址信息存于addr中(注意这里第三个参数是指针,而bind中是变量)。

accept函数返回的socket描述符是真正可以和客户端通信的socket,服务器的侦听socket只接受连接,不能用于通信。accept函数在没有已完成的连接时(即没有请求时)将阻塞进程。


下面两个基本读写函数准确说不再属于网络编程范畴,它们是从一个文件描述符中读写数据,属于linux基本文件读写操作。

read

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
  
  
  • 1
  • 2

fd中读取count个字节到buf中,成功时返回读取到的字节,文件指针也随之移动相应距离(很好理解)。

接收缓冲区中没有数据时read函数阻塞,出现下列情况时返回:

  • 收到数据
  • 连接被关闭,返回0
  • 连接被复位,返回错误
  • 阻塞过程中收到中断信号,errno=EINTR

write

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
  
  
  • 1
  • 2

buf中向fd中写入count个字节,成功时返回实际写入的字节数。

发送缓冲区中空间小于参数len时write函数阻塞 :

  • write函数阻塞时可能因为下列原因返回
  • 发送缓冲区中空间大于参数len
  • 连接被复位,返回错误
  • 阻塞过程中收到中断信号,返回EINTR

close

#include <unistd.h>
int close(int fd);
  
  
  • 1
  • 2

调用close只是将对sockfd的引用减1,直到对sockfd的引用为0时才清除sockfd ,TCP协议将继续使用sockfd,直到所有数据发送完成 .

高级socket函数

域名与ip转化

由域名得到结构

#include <netdb.h>
struct hostent *gethostbyname(const char *name);
  
  
  • 1
  • 2

查询域名对应的IP。

返回值是一个结构指针,如果为NULL表示出错。这个结构原型如下:

struct hostent{
    char     h_name;    /*主机正式名称*/
    char    **h_aliases;    /*别名列表,以NULL结束*/
    int     h_addrtype; /*主机地址类型:AF_INET*/
    int     h_length;   /*主机地址长度:4字节32位*/
    char    **h_addr_list;  /*主机网络地址列表,以NULL结束*/
}
#define     h_addr  h_addr_list[0]; //主机的第一个网络地址
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

一个程序示例:

//
// Created by prime on 17-6-19.
//

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

int main()
{
    struct hostent *he=gethostbyname("www.sina.com");
    int i;
    if(he!=NULL){
        printf("h_name:%s\n",he->h_name);
        printf("h_length:%d\n",he->h_length);
        printf("h_addrtype:%d",he->h_addrtype);
        for(i=0;he->h_aliases[i] !=NULL;i++)
            printf("h_aliases%d:%s\n",i+1,he->h_aliases[i]);
        //列出所有地址
        for(i=0;he->h_addr_list[i]!=NULL;i++){
            struct in_addr *addr;
            addr=(struct in_addr *)he->h_addr_list[i];
            printf("ip%d:%s\n",(i+1),inet_ntoa(*addr));
        }
    }
    else
        printf("gethostbyname error:%s\n",hstrerror(h_errno));

    return 0;
}

  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

由ip得到结构

#include <sys/socket.h>       /* for AF_INET */
struct hostent *gethostbyaddr(const void *addr,socklen_t len, int type);
  
  
  • 1
  • 2
struct in_addr addr;

inet_aton(“202.117.112.10”,&addr);
struct hostent *he=gethostbyaddr((void*)&addr,sizeof(addr),AF_INET);
if(he!=NULL){
    printf("h_name:%s\n",he->h_name);
}
else
    printf("gethostbyaddr error:%s\n",hstrerror(h_errno));
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

程序输出如下:

h_name:old.xidian.edu.cn
  
  
  • 1

send

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
  
  
  • 1
  • 2
  • 3
  • 4

send只能用于已经connect的套接字(不一定是tcp,udp也行),而对于sendto,如果已经connect了,那么后两个参数被忽略,即只能向连接的套接字发送数据。

发送数据时,接收方的地址放于dest_addr中。

成功时,返回发送的字节数。

当flag为0时,就等于write了。同理,下面的recv的flag为0就等于read了。

常用flag如下:

含义
MSG_DONTWAIT非阻塞操作,立刻返回不等待
MSG_OOB接受带外数据
MSG_PEEK查看数据,不进行数据缓冲区的
MSG_DONTROUTEsend特有,发送数据不查找路由表,适用于局域网,或同一网段
MSG_WAITALLrecv特有,阻塞直到所有的请求被满足。

recv

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
  
  
  • 1
  • 2
  • 3
  • 4

它们既可用于面向连接的,又可用于面向无连接的socket。返回收到的字节数。它们默认都是阻塞型的,包括上面的send也是。

接收数据时发送方的地址填充于src_addr中 。

shutdown

#include <sys/socket.h>
int shutdown(int sockfd, int how);
  
  
  • 1
  • 2

关闭全双工套接字的一方或者全部,成功时返回0.

how有以下几个值:

含义
SHUT_RD0停止接受
SHUT_WR1停止发送
SHUT_RDWR2停止收发

和close的区别如下:

  • shutdown操作连接通道,其他进程不能再使用已被关闭的通道;close操作描述符,其他进程仍然可以使用该socket描述符
  • close关闭应用程序与socket的接口,调用close之后进程不能再读写这个socket;shutdown可以只关闭一个通道,另一个通道仍然可以操作

bind、listen、创建都不会阻塞。

一个TCP的例子

服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8888                       /*侦听端口地址*/
#define BACKLOG 2                       /*侦听队列长度*/

extern void process_conn_server(int s);

int main(int argc, char *argv[])
{
    int ss,sc;      /*ss为服务器的socket描述符,sc为客户端的socket描述符*/
    struct sockaddr_in server_addr; /*服务器地址结构*/
    struct sockaddr_in client_addr; /*客户端地址结构*/
    int err;                            /*返回值*/
    pid_t pid;                          /*分叉的进行ID*/

    /*建立一个流式套接字*/
    ss = socket(AF_INET, SOCK_STREAM, 0);
    if(ss < 0){                         /*出错*/
        printf("socket error\n");
        return -1;  
    }

    /*设置服务器地址*/
    bzero(&server_addr, sizeof(server_addr));           /*清零*/
    server_addr.sin_family = AF_INET;                   /*协议族*/
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);    /*本地地址*/
    server_addr.sin_port = htons(PORT);             /*服务器端口*/

    /*绑定地址结构到套接字描述符*/
    err = bind(ss, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if(err < 0){/*出错*/
        printf("bind error\n");
        return -1;  
    }

    /*设置侦听*/
    err = listen(ss, BACKLOG);
    if(err < 0){                                        /*出错*/
        printf("listen error\n");
        return -1;  
    }

        /*主循环过程*/
    for(;;) {
        socklen_t addrlen = sizeof(struct sockaddr);

        sc = accept(ss, (struct sockaddr*)&client_addr, &addrlen); 
        /*接收客户端连接*/
        if(sc < 0){                         /*出错*/
            continue;                       /*结束本次循环*/
        }   

        /*建立一个新的进程处理到来的连接*/
        pid = fork();                       /*分叉进程*/
        if( pid == 0 ){                     /*子进程中*/
            process_conn_server(sc);        /*处理连接*/
            close(ss);                      /*在子进程中关闭服务器的侦听*/
        }else{
            close(sc);                      /*在父进程中关闭客户端的连接*/
        }
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

客户端

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8888                               /*侦听端口地址*/

extern void process_conn_client(int s);

int main(int argc, char *argv[])
{
    int s;                                      /*s为socket描述符*/
    struct sockaddr_in server_addr;         /*服务器地址结构*/

    s = socket(AF_INET, SOCK_STREAM, 0);        /*建立一个流式套接字 */
    if(s < 0){                                  /*出错*/
        printf("socket error\n");
        return -1;
    }   

    /*设置服务器地址*/
    bzero(&server_addr, sizeof(server_addr));   /*清零*/
    server_addr.sin_family = AF_INET;                   /*协议族*/
    //server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  /*本地地址*/
    server_addr.sin_port = htons(PORT);             /*服务器端口*/

    /*将用户输入的字符串类型的IP地址转为整型*/
    inet_aton("127.0.0.1", &server_addr.sin_addr);
    /*连接服务器*/
    connect(s, (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
    process_conn_client(s);                     /*客户端处理过程*/
    close(s);                                   /*关闭连接*/
    return 0;

}


  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

关键处理进程

#include <stdio.h>
#include <string.h>
#include <unistd.h>

/*客户端的处理过程*/
void process_conn_client(int s)
{
    ssize_t size = 0;
    char buffer[1024];                          /*数据的缓冲区*/

    for(;;){                                    /*循环处理过程*/
        /*从标准输入中读取数据放到缓冲区buffer中*/
        size = read(0, buffer, 1024);
        if(size > 0){                           /*读到数据*/
            write(s, buffer, size);             /*发送给服务器*/
            size = read(s, buffer, 1024);       /*从服务器读取数据*/
            write(1, buffer, size);             /*写到标准输出*/
        }
    }   
}
/*服务器对客户端的处理*/
void process_conn_server(int s)
{//s是新创建的用于socket通信的描述符
    ssize_t size = 0;
    char buffer[1024];                          /*数据的缓冲区*/

    for(;;){                                    /*循环处理过程*/      
        size = read(s, buffer, 1024);           /*从套接字中读取数据放到缓冲区buffer中*/
        if(size == 0){                          /*没有数据*/
            return; 
        }
        /*构建响应字符,为接收到客户端字节的数量*/
        sprintf(buffer, "%d bytes altogether\n", size);
        write(s, buffer, strlen(buffer)+1);/*发给客户端*/
    }   
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

一个UDP的例子

client

//
// Created by prime on 17-6-20.
//

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

#define PORT 8888

void process(int s, struct sockaddr *server)
{
    struct sockaddr_in client;
    char buff[1024]="Client:hello";
    socklen_t len= sizeof(*server);
    sendto(s,buff,strlen(buff),0,server,len);
    memset(buff, 0,sizeof(buff));
    ssize_t t=recvfrom(s,buff,1024,0,(struct sockaddr*)&client,&len);
    printf("%ld\n",t);
    write(1,buff,t);
}

int main()
{
    int s;
    struct sockaddr_in addr_server;
    s=socket(AF_INET,SOCK_DGRAM,0);

    bzero(&addr_server, sizeof(addr_server));
    addr_server.sin_family=AF_INET;
    addr_server.sin_port=htons(PORT);
    addr_server.sin_addr.s_addr=htonl(INADDR_ANY);
    process(s,(struct sockaddr *)&addr_server);
    close(s);
    return 0;
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

server

//
// Created by prime on 17-6-20.
//
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <zconf.h>

#define PORT 8888

void process(int s, struct sockaddr *client)
{
    ssize_t n;
    char buff[1024];
    socklen_t len;
    len= sizeof(*client);
    while(true)
    {
        n=recvfrom(s,buff,1024,0,client,&len);
        sendto(s,buff,n,0,client,len);
    }
}
int main(int  argc,char *argv[])
{
    int s;
    struct sockaddr_in addr_server,addr_client;
    s=socket(AF_INET,SOCK_DGRAM,0);
    bzero(&addr_server, sizeof(addr_server));

    addr_server.sin_family=AF_INET;
    addr_server.sin_port=htons(PORT);
    addr_server.sin_addr.s_addr=htonl(INADDR_ANY);

    bind(s,(struct sockaddr*)&addr_server, sizeof(addr_server));
    process(s,(struct sockaddr*)&addr_client);
    close(s);
    return 0;
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

UDP和TCP不同,常用sendto和recvfrom函数。

知识补充

bzero

#include <strings.h>
void bzero(void *s, size_t n);
//对以s开头的,n个字节进行0填充
  
  
  • 1
  • 2
  • 3

INADDR_ANY

/* Address to accept any incoming messages.  */
#define INADDR_ANY      ((in_addr_t) 0x00000000)
  
  
  • 1
  • 2

该宏定义为”0.0.0.0”,意义代表这台计算机的任意地址。

设置和获取套接字选项

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
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);
  
  
  • 1
  • 2
  • 3
  • 4

它们的主要作用是用来获取或设置与某个套接字关联的选项,选项可能存在于多层协议中。当对套接字选项进行操作时,必须给出选项所在的层和选项的名称。

比如为了操作套接字API层的选项,应该讲该层指定为SOL_SOCKET。为了操作其它层的选项,应该给出控制该选项的协议类型号。

参数具体含义如下:

  • sockfd套接字描述符
  • level选项所在协议层
  • optname选项名字
  • optval操作的内存缓冲区。对于第一个函数,指向用于获取选项值的缓冲区;对于后者,指向设置的参数的缓冲区。
  • optlen上一个参数的长度。

大多数选项用一个int参数作为optval,非0表示开启,0表示关闭。

根据level值的不同,选项大致可以分为三类:

  1. 通用套接字选项

    值为SOL_SOCKET,用于设置一些通用的参数。常用选项名有:SO_REUSEADDR地址重用

  2. IP选项

    值为IPPROTO_IP,用于设置或获取IP层的参数。

  3. TCP选项

    值为IPPROTO_TCP,设置TCP协议的一些具体参数。

翻译自以下文档:

getsockopt() and setsockopt() manipulate options for the socket referred to by the file descriptor sockfd.

Options may exist at multiple protocol levels; they are always present at the uppermost socket level.

When manipulating socket options, the level at which the option resides and the name of the option must be

specified. To manipulate options at the sockets API level, level is specified as SOL_SOCKET. To manipulate
options at any other level the protocol number of the appropriate protocol controlling the option is supplied.
For example, to indicate that an option is to be interpreted by the TCP protocol, level should be set to the
protocol number of TCP; see getprotoent(3).

The arguments optval and optlen are used to access option values for setsockopt(). For getsockopt() they
identify a buffer in which the value for the requested option(s) are to be returned. For getsockopt(), optlen
is a value-result argument, initially containing the size of the buffer pointed to by optval, and modified on
return to indicate the actual size of the value returned. If no option value is to be supplied or returned,
optval may be NULL.

Optname and any specified options are passed uninterpreted to the appropriate protocol module for interpreta‐
tion. The include file


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值