Windows socket基础

                    Windows socket 基础

 

     Windows socket是一套在Windows操作系统下的网络编程接口。它不是一种网络协议,而是一个开放的、支持多个协议的Windows下的网络编程接口

     Windows socket是以Unix socket为基础,因此Windows socket中的许多函数名与Unix都是一样的。除此之外它还允许开发人员充分利用Windows的消息驱动机制进行程序设计开发。

 

     套接字是应用层到运输层的接口。套接字用以表示一条连接的两端。每一个端点由ip和端口组成。因此套接字是由两端点的ip和端口组成。

 

     端口是运输层的概念,每个端口对应一个进程。因此一条连接表示一个进程与另一个进程建立联系。

 

     应用程序可以使用两种套接字。流套接字和数据包套接字。分别对应TCP和UDP。

 

     TCP提供面向连接的可靠的、无重复、有序的数据流服务。而UDP提供面向数据包的,不保证数据是可靠的、有序的和无重复的。

     在Windows环境下,使用Windows socket api进行网络程序开发时,需要调用Windows操作系统的Windows socket动态库。在应用程序中需要包含Windows sockets头文件。windows sockets 2.2版本需要包含WINSOCK2.h头文件(不区分大小写)。同时还需要添加动态库。一种是在头文件中添加。如:

     #pragma  comment(lib,"WS2_32.lib")

     另一种是在vc中添加。可以选择project -》settting,在link标签下添加wsock32.lib字符串。

 

     Windows sockets中定义的套接字类型SOCKET来表示套接字:

 

typedef unsigned int u_int;

typedef u_int SOCKET;


 

 

      其实所谓的SOCKET的类型只不过是unsigned int的别名罢了。INVALID_SOCKET表示一个无效的套接字,除此之外的0--INVALID_SOCKET-1都表示一个有效的套接字。因此在创建套接字后,都需要与INVALID_SOCKET比较,看创建的套接字是否有效。

SOCKET s=socket(...);

if(INVALID_SOCKET==s)

{

  //创建失败。

}


 

 

     Windows SOCKET可以支持多种不同的网络协议,并且提供与协议无关的编程接口。因此开发人员就可以相同的格式开发使用任一协议的网络应用程序,而不去关心各种协议的不同。

 

     每种协议都有一套不同的IP定址方案(即表示主机地址的方式)。TCP协议和UDP协议通过IP协议传输数据。

 

     而Windows SOCKET通过AF_INET地址家族为IP协议定址。

 

 

#define AF_INET 2


 

     网络中每台主机都有一个IP地址,用32位数字来表示。TCP和UDP必须指定端口号。在Windows SOCKET中sockaddr_in 结构被用来指定IP和端口号。

 

 

struct sockaddr_in

{

   short sin_family;

   u_short sin_port;

   struct in_addr sin_addr;

   char sin_zero[8];

};


 

     sin_family表示地址家族。使用TCP/IP协议的应用程序必须为aF_INET,来告诉系统使用IP地址家族 。

     sin_port指定服务的端口号。1024--49151范围内的数据被作为服务端口号,可以由用户自定义。    sin_zero字段作为填充字段。以便使得该结构与SOCKADDR结构长度相同。

in_addr的定义如下:

 

struct in_addr {       

         union {                

                struct{

                        u_char s_b1,s_b2,s_b3,s_b4; 

                       }S_un_b;       

                struct {

                       u_short s_w1,s_w2; 

                      } S_un_w;  

                u_long S_addr;   

             } S_un;

          }


 

     很显然它是一个存储ip地址的联合体,有三种表达方式:

 

     第一种用四个字节来表示IP地址的四个数字;

     第二种用两个双字节来表示IP地址;

     第三种用一个长整型来表示IP地址。

 

     给in_addr赋值的一种最简单方法是使用inet_addr函数,它可以把一个代表IP地址的字符串赋值转换为in_addr类型,如addrto.sin_addr.s_addr=inet_addr("192.168.0.2");

 

     其反函数是inet_ntoa,可以把一个in_addr类型转换为一个字符串。

 

      sockaddr类型sockaddr类型是用来表示socket地址的类型,同上面的sockaddr_in类型相比,sockaddr的适用范围更广,因为sockaddr_in只适用于TCP/IP地址。

 

sockaddr的定义如下:

 

 

struct sockaddr { 

       u_short sa_family;

      char  sa_data[14];

};  


 

     可知sockaddr有16个字节,而sockaddr_in也有16个字节,所以sockaddr_in是可以强制类型转换为sockaddr的。事实上也往往使用这种方法。

     不同cpu处理多字节时处理方式不同。Intel x86cpu对多字节的处理方式为高对高低对低。但是在网络上采用的是高对低,低对高的方式。因此也就存在所谓主机字节序和网络字节序的处理问题。

 

     Htonl和htons函数实现主机字节顺序和网络字节序的转换功能。H代表host,主机。N代表net。L代表long。S代表short。不要使用htonl转换short哦,楼主就有一次犯了这个错误,找了好久才找到原因。

 

当然也有从网络字节序到主机字节序的转换函数:ntohl和ntohs。

 

     除了支持TCP/IP协议之外,Windows SOCKET还支持IPX/SPX、ATM和红外线通信协议等等,它们都有自己的定址方法,感兴趣的同学可以参考其他资料。

 

     基本的套接字编程

     在使用套接字进行编程之前,无论是服务器还是客户端都必须加载Windows SOCKET动态库。函数WSAStartup就实现了此功能。它是套接字应用程序必须调用的第一个函数。

 

 

int WSAStartup(

   WORD wVersionRequested,

   LPWSADATA lpwsadata);


 

     第一个参数指定准备加载的Windows SOCKET动态库的版本。一般使用MAKEWORD构造。如MAKEWORD(2,2)表示加载2.2版本。

 

      WSADATA会返回被加载动态链接库的基本信息。如是否加载成功,以及当前实际使用的版本。具体结构不再介绍。

 

     初始化socket之后,需要创建套接字。socket函数和WSASocket函数可以实现此功能。

 

 

SOCKET socket(

               int af,

               int type,

               int protocao);


 

af表示使用协议的地址家族。创建TCP或UDP的套接字是使用AF_INET。

 

type表示套接字的类型。有SOCK_STREAM、SOCK_DGRAM和SOCK_RAM三种类型。分别表示流、数据包、原始套接字。

 

protocol,指示使用的协议。对于SOCK_STREAM套接字类型,该字段可以为IPPROTO_TCP或0。对于SOCK_DGRAM套接字类型,该字段为IPPROTO_UDP或0。

 

当函数创建成功时,返回一个新建的套接字句柄。否则将返回INVALID_SOCKET。如

 

 

SOCKET s=socket(AF_INET,SOCK_STREAM,0);

if(INVALID_SOCKET)

{

   //失败。

}


 

bind函数

 

在创建套接字之后就需要调用bind函数将其绑定到一个已知的地址上。

 

 

int bind(SOCKET s,

       const struct sockaddr*name,

       int namlen);


 

s为要绑定的套接字,name为要绑定的地址。namelen为sockaddr长度。

 

函数调用成功将返回0,否则返回值为SOCKET_ERROR。如果程序不关心分配给它的地址,可使用INADDR_ANY或将端口号设为0。端口号为0时,Windows SOCKET将给应用程序分配一个值在1024-5000之间唯一的端口号。

 

示例:

 

struct sockaddr_in addr;

int nServerPort=5500;

int nErrorCode;

addr.family=AF_INET;

addr.port=htons(nServerPort);

addr.sin_addr.S_addr=htonl(INADDR_ANY);

SOCKET s=socket(AF_INET,SOCK_STREAM,0);

if(s==INVALID_SOCKET)

{

  //失败。

}

nErrorCode=bind(s,(SOCKADDR*)&addr,sizeof(addr));

if(nErrorCode==SOCKET_ERROR)

{

  //失败。



}


 

listen函数将套接字设定为监听模式。

 

 

int listen (

   SOCKET s,

   int backlog);


 

s为要设置监听模式的套接字。

backlog指定等待连接的最大队列长度。

当函数成功时返回0,否则返回SOCKET_ERROR。

假如backlog为3,则说明最大等待连接的最大值为3.如果有四个客户端同时向服务器发起请求,那么第四个连接将会发生WSAEWOULDBLOCK错误。当服务器接受了一个请求,就将该请求从请求队列中删去。

 

 

int ret=listen(s,3);

if(ret==SOCKET_ERROR)

{

   //失败。
}


 

accept函数接受客户端的一个连接请求。

 

 

SOCKET accept{

     SOCKET s,

     struct sockaddr*addr,

     int *addrlen);


 

s为监听套接字。

 

addr返回客户端地址。

 

addrlen返回addr的长度。

 

函数执行成功时:

 

1:主机接受了等待队列的第一个请求。

 

2:addr结构返回客户端地址。

 

3:返回一个新的套接字句柄。服务器可以使用该套接字与客户端进行通信。而监听套接字仍用于接受客户端连接。

 

SOCKET sListen;//监听套接字。

SOCKET sAccept;//接受套接字。

sockaddr_in addrClient;//客户端地址。

int addrClientLen=sizeof(addrClient);

sAccept=accept(sListen,(SOCKADDR*)&addrClient,&addrClientLent);

if(INVALID_SOCKET==sAccept)

{

   //失败。

}


 

 

recv()函数

recv()和WSARecv()函数用于接收数据。

 

int recv(

      SOCKET s,

      char *buf,

      int len,

      int flags);


 

s为接收数据套接字。

 

buff接受缓冲区。

 

len缓冲区长度。

 

flags:该参数影响函数的行为。它可以是0,MEG_PEEK和MSG_OOB。0表示无特殊行为。MSG_PEEK会使有用的数据被复制到buff中,但没有从系统缓冲区内将这些数据删除。MSG_OOB表示处理外带数据。

 

recv函数返回接收的字节数。当函数执行失败时返回SOCKET_ERROR。

 

 

SOCKET s;

char buff[128];

nReadlen=recv(s,buff,128,0);

if(SOCKET_ERROR==nReadlen)

{

}


 

send函数

send()和WSASend()用于发送数据。

 

 

int send(

    SOCKET s,

    const char *buff,

    int len,

    int flags);


 

s为发送套接字。

 

buff为发送缓冲区。

 

len发送数据长度。

 

flags可以是0或MSG_DONTROUTE或MSG_OOB。0表示无特殊行为。MSG_DONTROUTEyaoqiu传输层不要将此数据路由出去。MSG_OOB表示该数据应该被外带发送。

 

函数成功时返回实际发送的字节数。失败返回SOCKET_ERROR。

 

 

SOCKET s;

char buff[128];

int ret;

strcpy(buff,”sendData”);

ret=send(s,strlen(buff),0);

if(SOCKET_ERROR==ret)

{

}


 

closesocket()函数

 

closesocket()函数用以关闭套接字。释放套接字所占资源。

 

 

int closesocket(

    SOCKET s);


 

调用过closesocket函数的套接字继续使用时会返回WSAENOTSOCK错误。

 

shutdown()函数。

 

Shutdown()函数用于通知对方不再发送数据或者不再接收数据或者既不发送也不接收数据。

 

 

int shutdown(

   SOCKET s,

   int how);


 

how参数可以是:

SD_RECEIVE表示不再接收数据,不允许再调用接收数据函数。

SD_SEND表示不再发送数据。

SD_BOTH表示既不发送也不接收数据。

 

 

connect函数实现连接服务器的功能。

 

int connect(

       SOCKET s,

       const struct sockaddr*name,

       int namelen);


    s为套接字。

    name为服务器地址。

    namelen为sockaddr结构长度。

    函数执行成功返回0,否则返回SOCKET_ERROR。

 

 

SOCKET s;

ULONG ulServIp;

USHORT ServPort;

int ret;

SOCKADDR_IN servAddr;

servAddr.sin_family=AF_INET;

servAddr.sin_addr.S_addr=htonl(ulServIp);

servAddr.sin_port=htons(ServPort);

int len=sizeof(servaddr);

ret=connect(s,(SOCKADDR*)&servAddr,sizeof(servAddr));

if(ret==SOCKET_ERROR)

{
}


 

     创建套接字后,可以对它的各种属性进行设置。可以调用getsockopt()函数来返回套接字选项信息。setsockopt()设置套接字选项。

 

getsockopt()函数

 

它用于获取套接字选项信息:

 

 

int getsockopt(

SOCKET s,

int level,

int optname,

char *optval,

int optlen);


 

s为要取得选项的套接字。

level为选项级别,有SOL_SOCKET和IPPROTO_TCP两个级别。

optname套接字选项名称。

optval该参数返回套接字选项名称对应的值。

optlen为缓冲区optval大小。

函数成功,返回值为0。否则返回SOCKET_ERROR。

 

int Bufflen;

int noptlen=sizeof(nBufflen);

int ret=getsockopt(s,SOL_SOCKET,SO_RCVBUF,(char*)&BuffLen,&noptlen);

if(ret==SOCKET_ERROR)

{

}


 

setsockopt函数。

 

它可以设置套接字选项。若不能正确设置socket属性,则数据的发送和接收会失败

 

 

int setsockopt(

              SOCKET s,

              int level,

              int optname,

              char *optval,

              int optlen);


 

s为要取得选项的套接字。

 

level为选项级别,有SOL_SOCKET和IPPROTO_TCP两个级别。

optname套接字选项名称。

optval参数设置套接字选项的值。

optlen为optval大小。

 

下列代码首先调用getsockopt函数获得默认接收缓冲区的大小,然后调用setsockopt将接收缓冲区大小设置为原来的10倍。再次调用getsockopt来检查是否设置成功。

 

 

int opt;

int noptlen=sizeof(opt);

int ret=getsockopt(s,SOL_SOCKET,SO_RECVBUFF,(char*)&opt,noptlen);

if(ret==SOCKET_ERROR)

{

}


opt*=10;

ret=setsockopt(s,SOL_SOCKET,SO_RECVBUFF,(char*)&opt,noptlen);

if(ret==SOCKET_ERROR)

{

}

int newopt;

getsockopt(s,SOL_SOCKET,SO_RECVBUFF,(char*)&newopt,&noptlen);

if(newopt!=opt)

{

  //设置失败。
}


 

当设置SOL_SOCKET选项级别时,调用setsockopt函数和getsockopt函数所设置或获取的信息为套接字本身的特征,这些信息与基层协议无关。

 

SOL_SOCKET级别包括一下类型:

SO_ACCEPTCONN      bool类型。如果为真,表明套接字处于监听模式。

SO_BROADCAST       bool类型,如果为真,表明套接字已设置成广播消息发送。

SO_DEBUG           bool类型。如果为真,则允许输出调试信息。

SO_DONTLINGER      bool类型。如果为真,则禁止SO_LINGER。

SO_DONTROUTE       bool类型。如果为真,则不会做出路由选择。

SO_ERROR           int 类型。返回和重设以具体套接字为基础的错误代码。

SO_KEEKPALIVE       bool类型。如果为真,则套接字在会话过程中会发送保持活动消息。

SO_LINGER           struct linger*类型,设置或获取当前的拖延值。

SO_OOBINLINE        bool如果为真,则带外数据会在普通数据流中返回。

SO_RECVBUF          int类型。设置或获取接收缓冲区长度。

SO_REUSEADDR        bool类型。如果为真,套接字可以与一个正在被其他套接字使用的地址绑定。

SO_SNDBUF           bool类型。设置或获取发送缓冲区大小。

SO_TYPE            int类型。返回套接字类型,如SOCK_DGREAM,SOCK_STREAM。

SO_SNDTIMEO         int类型。设置或获取套接字在发送数据的超时时间。

SO_RECVTIMEO        int类型,设置或获取套接字在接收数据的超时时间。

 

     WSAGetLastError函数
      该函数用来在Socket相关API失败后读取错误码,根据这些错误码可以对照查出错误原因。

        GetComputerName()用来获取计算机名称:

BOOL GetComputerName( LPTSTR lpBuffer, LPDWORD lpnSize);

plBuffer是用来存储返回的名称的缓冲区。

     lpBuffer为缓冲区。

     lpnSize为缓冲区大小。同时它也返回计算机名称的长度。

     此函数不属于winsock库函数。使用之前不需要初始化库。

     使用方法为:

<span style="font-size: 18px;">    TCHAR szHostName[20];
    DWORD dwSize = 20;
    GetComputerName( szHostName, &dwSize );

</span>

 

     gethostname函数:

     此函数为WinSock库函数,使用之前需要初始化WinSock库。

    int  gethostname(char  *name, int namelen);

     name为存储主机名缓冲区。

     namelen为缓冲区长度。

以上参考自《Windows sockets网络开发--基于Visual C++实现》如有纰漏,请不吝赐教!!!

                                            2012.12.28于山西大同

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Windows Sockets 规范以 U.C. Berkeley 大学 BSD UNIX 中流行的 Socket 接口为范例定义 了一套 Micosoft Windows 下网络编程接口。它不仅包含了人们所熟悉的 Berkeley Socket 风 格的库函数;也包含了一组针对 Windows 的扩展库函数,以使程序员能充分地利用 Windows 消息驱动机制进行编程。 Windows Sockets 规范本意在于提供给应用程序开发者一套简单的 API,并让各家网络 软件供应商共同遵守。此外,在一个特定版本 Windows基础上,Windows Sockets 也定义 了一个二进制接口(ABI),以此来保证应用 Windows Sockets API 的应用程序能够在任何 网络软件供应商的符合 Windows Sockets 协议的实现上工作。因此这份规范定义了应用程序 开发者能够使用,并且网络软件供应商能够实现的一套库函数调用和相关语义。 遵守这套 Windows Sockets 规范的网络软件,我们称之为 Windows Sockets 兼容的,而 Windows Sockets 兼容实现的提供者,我们称之为 Windows Sockets 提供者。一个网络软件供 应商必须百分之百地实现 Windows Sockets 规范才能做到现 Windows Sockets 兼容。 任何能够与 Windows Sockets 兼容实现协同工作的应用程序就被认为是具有 Windows Sockets 接口。我们称这种应用程序为 Windows Sockets 应用程序。 Windows Sockets 规范定义并记录了如何使用 API 与 Internet 协议族(IPS,通常我们指 的是 TCP/IP)连接,尤其要指出的是所有的 Windows Sockets 实现都支持流套接口和数据报 套接口. 应用程序调用 Windows Sockets 的 API 实现相互之间的通讯。Windows Sockets 又利用 下层的网络通讯协议功能和操作系统调用实现实际的通讯工作。
一个简单的socket网络编程例子: 服务器代码: #include #include #include #include #pragma comment(lib,"ws2_32.lib") //这句话的意思是加载ws2_32.lib这个静态库 #define NETWORK_EVENT WM_USER+100 //如果你用mfc做开发,你可以点击菜单project-〉setting-〉link-〉object/library中添加这个静态库。 //如果你用c语言,你需要通过#pragma comment(命令来连接静态库 int main(int argc, char* argv[]){ HANDLE hThread = NULL; //判断是否输入了端口号 if(argc!=3){ printf("Usage: %sPortNumber\n",argv[1]); exit(-1); } //把端口号转化成整数 short port; if((port = atoi(argv[2]))==0){ printf("端口号有误!"); exit(-1); } WSADATA wsa; //初始化套接字DLL if(WSAStartup(MAKEWORD(2,2),&wsa)!=0){ //高字节指定了次版本号,低字节指定了主版本号,两个字节加到一起,就是你想要的Winsock库的版本号了 printf("套接字初始化失败!"); exit(-1); } //创建套接字 SOCKET serverSocket; if((serverSocket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))==INVALID_SOCKET){ printf("创建套接字失败!"); exit(-1); } struct sockaddr_in serverAddress; memset(&serverAddress,0,sizeof(sockaddr_in)); serverAddress.sin_family=AF_INET; serverAddress.sin_addr.S_un.S_addr = htonl(INADDR_ANY); serverAddress.sin_port = htons(port); //绑定 if(bind(serverSocket,(sockaddr*)&serverAddress,sizeof(serverAddress))==SOCKET_ERROR){ printf("套接字绑定到端口失败!端口: %d\n",port); exit(-1); } //进入侦听状态 if(listen(serverSocket,SOMAXCONN)==SOCKET_ERROR){ printf("侦听失败!"); exit(-1); } printf("Server %d is listening......\n",port); SOCKET clientSocket[5],maxSocket;//用来和客户端通信的套接字 struct sockaddr_in clientAddress;//用来和客户端通信的套接字地址 memset(&clientAddress,0,sizeof(clientAddress)); int addrlen = sizeof(clientAddress); fd_set fd_read; int i=0; int j; char buf[4096]; char buff[4096]="exit"; while(1) { FD_ZERO(&fd_read); maxSocket=serverSocket; FD_SET(serverSocket,&fd_read); //FD_SET(clientSocket[i-1],&fd_read); for(j=0;j<i;j++) { FD_SET(clientSocket[j],&fd_read); if(maxSocket"); //gets(buff); if(select(maxSocket+1,&fd_read,NULL,NULL,NULL)>0) { if(FD_ISSET(serverSocket,&fd_read)) { if(buff=="") { if((clientSocket[i++]=accept(serverSocket,(sockaddr*)&clientAddress,&addrlen))==INVALID_SOCKET) { printf("接受客户端连接失败!"); exit(-1); } else { for(j=0;j5) { printf("超过最大客户端数"); exit(-1); } } else { int bytes; for(int k=0;k<i;k++) { if(FD_ISSET(clientSocket[k],&fd_read)) { bytes=recv(clientSocket[k],buf,sizeof(buf),0); if(bytes==-1) { //listen(serverSocket,SOMAXCONN); for (int l=k;l<i;l++) clientSocket[l]=clientSocket[l+1]; i--; } /*if(bytes==0) { //printf("fdsdf"); listen(serverSocket,SOMAXCONN); for (int l=k;l0) { buf[bytes]='\0'; printf("Message from %s: %s\n",inet_ntoa(clientAddress.sin_addr),buf); if(send(clientSocket[k],buf,bytes,0)==SOCKET_ERROR) { printf("发送数据失败!"); exit(-1); } } } } } } } //清理套接字占用的资源 WSACleanup(); return 0; } 客户端代码: #include #include #include #pragma comment(lib,"ws2_32.lib") int main(int argc, char* argv[]){ //判断是否输入了IP地址和端口号 if(argc!=4){ printf("Usage: %s IPAddress PortNumber\n",argv[1]); exit(-1); } //把字符串的IP地址转化为u_long unsigned long ip; if((ip=inet_addr(argv[2]))==INADDR_NONE){ printf("不合法的IP地址:%s",argv[1]); exit(-1); } //把端口号转化成整数 short port; if((port = atoi(argv[3]))==0){ printf("端口号有误!"); exit(-1); } printf("Connecting to %s:%d......\n",inet_ntoa(*(in_addr*)&ip),port); WSADATA wsa; //初始化套接字DLL if(WSAStartup(MAKEWORD(2,2),&wsa)!=0){ printf("套接字初始化失败!"); exit(-1); } //创建套接字 SOCKET sock,serverSocket; if((sock=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP))==INVALID_SOCKET){ printf("创建套接字失败!"); exit(-1); } struct sockaddr_in serverAddress; memset(&serverAddress,0,sizeof(sockaddr_in)); serverAddress.sin_family=AF_INET; serverAddress.sin_addr.S_un.S_addr = ip; serverAddress.sin_port = htons(port); //建立和服务器的连接 if(connect(sock,(sockaddr*)&serverAddress,sizeof(serverAddress))==SOCKET_ERROR) { printf("建立连接失败!"); exit(-1); } char buf[4096]; while(1){ printf(">"); //从控制台读取一行数据 gets(buf); if(send(sock,buf,strlen(buf),0)==SOCKET_ERROR){ printf("发送c数据失败!"); exit(-1); } int bytes; if((bytes=recv(sock,buf,sizeof(buf),0))==SOCKET_ERROR) { printf("接收c数据失败!\n"); exit(-1); } else { buf[bytes]='\0'; printf("Message from %s: %s\n",inet_ntoa(serverAddress.sin_addr),buf); } } //清理套接字占用的资源 WSACleanup(); return 0; }

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值