socket编程

16 篇文章 0 订阅
10 篇文章 0 订阅

socket编程

socket这个词可以表示很多概念:

在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP 地址+端口号”就称为socket。

在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成 的socket pair就唯一标识一个连接。socket本身有“插座”的意思,因此用来描述网络连 接的一对一关系。

TCP/IP协议最早在BSD UNIX上实现,为TCP/IP协议设计的应用层编程接口称为socket

API。


本章的主要内容是socket API,主要介绍TCP协议的函数接口,最后介绍UDP协议和UNIX

Domain Socket的函数接口。

11.1 预备知识

11.1.1 网络字节序

  我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的
多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,
那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到
高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址
从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发

118 第11章 socket编程

出的数据是高地址。 TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。例如上一节的UDP

段格式,地址0-1是16位的源端口号,如果这个端口号是1000(0x3e8),则地址0是0x03, 地址1是0xe8,也就是先发0x03,再发0xe8,这16位在发送主机的缓冲区中也应该是低地址 存0x03,高地址存0xe8。但是,如果发送主机是小端字节序的,这16位被解释成0xe803,而 不是1000。因此,发送主机把1000填到发送缓冲区之前需要做字节序的转换。同样地,接收 主机如果是小端字节序的,接到16位的源端口号也要做字节序的转换。如果主机是大端字节 序的,发送和接收都不需要做转换。同理,32位的IP地址也要考虑网络字节序和主机字节序 的问题。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运 行,可以调用以下库函数做网络字节序和主机字节序的转换。

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

h表示host,n表示network,l表示32位长整数,s表示16位短整数。 

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回,如果主机是大端字节序,这些函数不做转 换,将参数原封不动地返回。

11.1.2 IP地址转换函数

早期

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp); in_addr_t inet_addr(const char *cp);

char *inet_ntoa(struct in_addr in);

只能处理IPv4的ip地址 不可重入函数 注意参数是struct in_addr

现在

#include <arpa/inet.h>

int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

支持IPv4和IPv6 可重入函数

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,

因此函数接口是void *addrptr。

11.1.3 sockaddr数据结构

strcut sockaddr 很多网络编程函数诞生早于IPv4协议,那时候都使用的是sockaddr结 构体,为了向前兼容,现在sockaddr退化成了(void *)的作用,传递一个地址给函数,至 于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部再强制类型转 化为所需的地址类型


图 11.1: sockaddr数据结构



Pv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包 括16位端口号和32位IP地址,IPv6地址用sockaddr_in6结构体表示,包括16位端口号、128 位IP地址和一些控制字段。UNIX Domain Socket的地址格式定义在sys/un.h中,用sock- addr_un结构体表示。各种socket地址结构体的开头都是相同的,前16位表示整个结构 体的长度(并不是所有UNIX的实现都有长度字段,如Linux就没有),后16位表示地址类 型。IPv4、IPv6和Unix Domain Socket的地址类型分别定义为常数AF_INET、AF_INET6、AF_UNIX。 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构 体,就可以根据地址类型字段确定结构体中的内容。因此,socket API可以接受各种类型的 sockaddr结构体指针做参数,例如bind、accept、connect等函数,这些函数的参数应该设 计成void *类型以便接受各种类型的指针,但是sock API的实现早于ANSI C标准化,那时还 没有void *类型,因此这些函数的参数都用struct sockaddr *类型表示,在传递参数之前 要强制类型转换一下,例如:

struct sockaddr_in servaddr;
/* initialize servaddr */
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));

11.2 网络套接字函数

            


图 11.2: socketAPI

11.2.1 socket

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

#include <sys/socket.h>

int socket(int domain, int type, int protocol);

domain:
AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
AF_INET6 与上面类似,不过是来用IPv6的地址
AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用

type:
SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类

型,这个socket是使用TCP来进行传输。
SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。 SOCK_SEQPACKET 这个协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的

接受才能进行读取。
SOCK_RAW 这个socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使

用该协议)
SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数

据包的顺序 protocol:

0 默认协议 返回值:

成功返回一个新的文件描述符,失败返回-1,设置errno

socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描 述符,应用程序可以像读写文件一样用read/write在网络上收发数据,如果socket()调 用出错则返回-1。对于IPv4,domain参数指定为AF_INET。对于TCP协议,type参数指定为 SOCK_STREAM,表示面向流的传输协议。如果是UDP协议,则type参数指定为SOCK_DGRAM,表示面向数据报的传输协议。protocol参数的介绍从略,指定为0即可。

11.2.2 bind

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

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

sockfd: socket文件描述符

addr: 构造出IP地址加端口号

addrlen: sizeof(addr)长度

返回值:
成功返回0,失败返回-1, 设置errno

服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序 的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络 地址和端口号。

bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件 描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类 型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需 要第三个参数addrlen指定结构体的长度。如:

struct sockaddr_in servaddr;
bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET;

 servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 

servaddr.sin_port = htons(8000);

首先将整个结构体清零,然后设置地址类型为AF_INET,网络地址为INADDR_ANY,这个 宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪 个IP地址,端口号为8000。

11.2.3 listen


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

 #include <sys/socket.h>

int listen(int sockfd, int backlog);

 sockfd:

socket文件描述符 

backlog:

排队建立3次握手队列和刚刚建立3次握手队列的链接数和

查看系统默认backlog

cat /proc/sys/net/ipv4/tcp_max_syn_backlog

典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的 accept()返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未 accept的客户端就处于连接等待状态,listen()声明sockfd处于监听状态,并且最多允许有 backlog个客户端处于连接待状态,如果接收到更多的连接请求就忽略。listen()成功返回 0,失败返回-1。

11.2.4 accept

11.2节 网络套接字函数 123


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

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); 

sockdf:

socket文件描述符 

addr:

传出参数,返回链接客户端地址信息,含IP地址和端口号

 addrlen:

传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构体的大小

 返回值:

成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

三方握手完成后,服务器调用accept()接受连接,如果服务器调用accept()时还没有 客户端的连接请求,就阻塞等待直到有客户端连接上来。addr是一个传出参数,accept() 返回时传出客户端的地址和端口号。addrlen参数是一个传入传出参数(value-result argument),传入的是调用者提供的缓冲区addr的长度以避免缓冲区溢出问题,传出的是客 户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给addr参数传 NULL,表示不关心客户端的地址。

  我们的服务器程序结构是这样的:

while (1) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len); 

n = read(connfd, buf, MAXLINE);
......
close(connfd);

}

整个是一个while死循环,每次循环处理一个客户端连接。由于cliaddr_len是传入传出 参数,每次调用accept()之前应该重新赋初值。accept()的参数listenfd是先前的监听文件

124 第11章 socket编程

描述符,而accept()的返回值是另外一个文件描述符connfd,之后与客户端之间就通过这个 connfd通讯,最后关闭connfd断开连接,而不关闭listenfd,再次回到循环开头listenfd仍 然用作accept的参数。accept()成功返回一个文件描述符,出错返回-1。

11.2.5 connect

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

 #include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 

sockdf:

socket文件描述符 

addr:

传入参数,指定服务器端地址信息,含IP地址和端口号

 addrlen:

传入参数,传入sizeof(addr)大小 

返回值:

成功返回0,失败返回-1,设置errno

客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于 bind的参数是自己的地址,而connect的参数是对方的地址。connect()成功返回0,出错返 回-1。

11.3 C/S模型-TCP

下图是基于TCP协议的客户端/服务器程序的一般流程:

        图 11.3: TCP协议通讯流程

服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于 监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服 务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK 段,服务器收到后从accept()返回。

数据传输的过程:

建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是 由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept() 返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客 户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处 理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发 回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一 条请求,如此循环下去。

如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样, 服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注 意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方 调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

在学习socket API时要注意应用程序和TCP协议层是如何交互的: *应用程序调用某个 socket函数时TCP协议层完成什么动作,比如调用connect()会发出SYN段 *应用程序如何知 道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段, 再比如read()返回0就表明收到了FIN段

11.3.1 server

下面通过最简单的客户端/服务器程序的实例来学习socket API。 

server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。

/* server.c */
#include <stdio.h>

 #include <stdlib.h> 

#include <string.h> 

#include <unistd.h>

 #include <sys/socket.h> 

#include <netinet/in.h> 

#include <arpa/inet.h>

#define MAXLINE 80

 #define SERV_PORT 8000

int main(void) {

struct sockaddr_in servaddr, cliaddr; 

socklen_t cliaddr_len;
int listenfd, connfd;
char buf[MAXLINE];

char str[INET_ADDRSTRLEN]; 

int i, n;

listenfd = socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr)); 

servaddr.sin_family = AF_INET; 

servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 

servaddr.sin_port = htons(SERV_PORT);

bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

listen(listenfd, 20);

printf("Accepting connections ...\n"); 

while (1) {

cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);

n = read(connfd, buf, MAXLINE);

 printf("received from %s at PORT %d\n",

inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), 

ntohs(cliaddr.sin_port));

for (i = 0; i < n; i++) 

    buf[i] = toupper(buf[i]);

write(connfd, buf, n);

close(connfd); 

}

}

11.3.2 client

client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的 字符串并打印。

/* client.c */
#include <stdio.h>

 #include <stdlib.h>

 #include <string.h> 

#include <unistd.h>

 #include <sys/socket.h> 

#include <netinet/in.h>

#define MAXLINE 80 #define SERV_PORT 8000

int main(int argc, char *argv[]) {

struct sockaddr_in servaddr; 

char buf[MAXLINE];
int sockfd, n;
char *str;

if (argc != 2) {
fputs("usage: ./client message\n", stderr); 

exit(1);

}
str = argv[1];

sockfd = socket(AF_INET, SOCK_STREAM, 0);

bzero(&servaddr, sizeof(servaddr)); 

servaddr.sin_family = AF_INET;

inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

 servaddr.sin_port = htons(SERV_PORT);

connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)); 

write(sockfd, str, strlen(str));

n = read(sockfd, buf, MAXLINE); printf("Response from server:\n"); 

write(STDOUT_FILENO, buf, n);

close(sockfd);

return 0; 

}

由于客户端不需要固定的端口号,因此不必调用bind(),客户端的端口号由内核自动分 配。注意,客户端不是不允许调用bind(),只是没有必要调用bind()固定一个端口号,服务 器也不是必须调用bind(),但如果服务器不调用bind(),内核会自动给服务器分配监听端 口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

  客户端和服务器启动后可以查看链接情况:

netstat -apn|grep 8000

11.4 C/S模型-UDP

图 11.4: UDP处理模型

由于UDP不需要维护连接,程序逻辑简单了很多,但是UDP协议是不可靠的,实际上有很 多保证通讯可靠性的机制需要在应用层实现。

编译运行server,在两个终端里各开一个client与server交互,看看server是否具有并 发服务的能力。用Ctrl+C关闭server,然后再运行server,看此时client还能否和server联 系上。和前面TCP程序的运行结果相比较,体会无连接的含义。

11.4.1 server

/* udp server.c */ 

#include <string.h> 

#include <netinet/in.h>

 #include <stdio.h> 

#include <unistd.h> 

#include <strings.h>

 #include <arpa/inet.h> 

#include <ctype.h>

 #define MAXLINE 80

 #define SERV_PORT 8000

 int main(void){

struct sockaddr_in servaddr, cliaddr; 

socklen_t cliaddr_len;

int sockfd;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, n;

sockfd = socket(AF_INET, SOCK_DGRAM, 0);

bzero(&servaddr, sizeof(servaddr)); 

servaddr.sin_family = AF_INET; 

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

 servaddr.sin_port = htons(SERV_PORT);

bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

printf("Accepting connections ...\n"); 

while (1) {

cliaddr_len = sizeof(cliaddr);
n = recvfrom(sockfd, buf, MAXLINE,0, (struct sockaddr *)&cliaddr, &cliaddr_len); 

if (n == -1)

perror("recvfrom error"); 

printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));

 for (i = 0; i < n; i++)

buf[i] = toupper(buf[i]);
n = sendto(sockfd, buf, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); 

if (n == -1)

perror("sendto error");

close(sockfd);

 return 0;

}

11.4.2 client

/* udp client.c */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <ctype.h>
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[]) {

struct sockaddr_in servaddr;

 int sockfd, n;
char buf[MAXLINE];

sockfd = socket(AF_INET, SOCK_DGRAM, 0);

bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);

 servaddr.sin_port = htons(SERV_PORT);

while (fgets(buf, MAXLINE, stdin) != NULL) {
n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));

 if (n == -1)

perror("sendto error");
n = recvfrom(sockfd, buf, MAXLINE, 0, NULL, 0);

 if (n == -1)

perror("recvfrom error");

 write(STDOUT_FILENO, buf, n);

close(sockfd);

 return 0;

}

11.5 出错处理封装函数

  上面的例子不仅功能简单,而且简单到几乎没有什么错误处理,我们知道,系统调用不
能保证每次都成功,必须进行出错处理,这样一方面可以保证程序逻辑正常,另一方面可以
迅速得到故障信息。

为使错误处理的代码不影响主程序的可读性,我们把与socket相关的一些系统函数加上 错误处理代码包装成新的函数,做成一个模块wrap.c:

11.5.1 wrap.c

/* wrap.c */
#include <stdlib.h> 

#include <errno.h> 

#include <sys/socket.h>

void perr_exit(const char *s) {

perror(s);

exit(1); }

int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr) {

int n;

again:
if ( (n = accept(fd, sa, salenptr)) < 0) {

  if ((errno == ECONNABORTED) || (errno == EINTR)) 

        goto again;

else

perr_exit("accept error");

}

return n; 

}

void Bind(int fd, const struct sockaddr *sa, socklen_t salen) {

    if (bind(fd, sa, salen) < 0) perr_exit("bind error");

}

void Connect(int fd, const struct sockaddr *sa, socklen_t salen) {

    if (connect(fd, sa, salen) < 0) perr_exit("connect error");

}

void Listen(int fd, int backlog) {

    if (listen(fd, backlog) < 0) perr_exit("listen error");

}

int Socket(int family, int type, int protocol) {

int n;

if ( (n = socket(family, type, protocol)) < 0) 

perr_exit("socket error");

return n; 

}

ssize_t Read(int fd, void *ptr, size_t nbytes)

{
ssize_t n;

again:
if ( (n = read(fd, ptr, nbytes)) == -1) {

if (errno == EINTR) 

goto again;

else
return -1;

}

return n; 

}

ssize_t Write(int fd, const void *ptr, size_t nbytes) {

ssize_t n;

again:
if ( (n = write(fd, ptr, nbytes)) == -1) {

if (errno == EINTR) 

goto again;

else
return -1;

}

return n; 

}

void Close(int fd) {

if (close(fd) == -1)

 perr_exit("close error");

}
ssize_t Readn(int fd, void *vptr, size_t n) {

size_t nleft; 

ssize_t nread; 

char *ptr;

ptr = vptr;
nleft = n;
while (nleft > 0) {

if ( (nread = read(fd, ptr, nleft)) < 0) { if (errno == EINTR)

nread = 0;

 else

return -1;

else if (nread == 0)

break;

nleft -= nread;

ptr += nread; }

return n - nleft; }

ssize_t Writen(int fd, const void *vptr, size_t n)

{
size_t nleft;

ssize_t nwritten; 

const char *ptr;

ptr = vptr;
nleft = n;
while (nleft > 0) {

if ( (nwritten = write(fd, ptr, nleft)) <= 0) {

if (nwritten < nwritten =

else
return -1;

}

nleft -= nwritten;

ptr += nwritten; }

return n; }

static ssize_t my_read(int {

0 && errno == EINTR) 0;

fd, char *ptr)

static int read_cnt; static char *read_ptr; static char read_buf[100];

if (read_cnt <= 0) { again:

if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) { if (errno == EINTR)

goto again; return -1;

} else if (read_cnt == 0) return 0;

read_ptr = read_buf; }

read_cnt--;
*ptr = *read_ptr++; return 1;

}

ssize_t Readline(int fd, void *vptr, size_t maxlen) {

ssize_t n, rc; char c, *ptr;

ptr = vptr;
for (n = 1; n < maxlen; n++) {

if ( (rc = my_read(fd, &c)) == 1) { *ptr++ = c;

if (c == '\n') break;
} else if (rc == 0) {

*ptr = 0;

return n - 1; } else

11.5.2 wrap.h

11.6节 练习 133

return -1;

}
*ptr = 0; return n;

}

/* wrap.h */ #ifndef __WRAP_H_ #define __WRAP_H_

void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr); void Bind(int fd, const struct sockaddr *sa, socklen_t salen); void Connect(int fd, const struct sockaddr *sa, socklen_t salen); void Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
void Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
static ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);

#endif


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值