网络编程学习

发布于:2024-05-08 ⋅ 阅读:(28) ⋅ 点赞:(0)

查本地ip地址

ipconfig

 

1 UDP编程概要

字节序

         多字节的顺序,分为小端存储和大端存储

        网络协议指定字节顺序是大端存储

字节序转换函数

htonl()

        功能:将32位主机字节序转换为网络字节序

        返回值:网络字节序的值

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostint32);

htons()

        功能:将16位主机字节序转换为网络字节序

        返回值:网络字节序的值

#include <arpa/inet.h>

uint16_t htonl(uint16_t hostint16);

ntohl()

        功能:将32位网络字节序转换为主机字节序

ntohs()

        功能:将32位网络字节序转换为主机字节序

地址转换函数

inet_pton()

        功能:字符串ip地址转整型数据,将点分十进制数串转为32位无符号整数

#include <arpa/inet.h>

int inet_pton(int family,const char *strptr,void *addrptr);

参数:

        family   协议族,如AF_INET  IPv4网络协议、AF_INET6  IPv6

        addrptr   32位无符号整数

        strptr      点分十进制数串

返回值:

        成功返回1,失败返回其他

例子:

#include <apra/inet.h>
int main(){
    char ip_str[]="198.168.3.103";
    unsigned int ip_int=0;
    unsigned char *ip_p=NULL;

    //将十进制转为32位无符号整型
    inet_pton(AF_INET,ip_str,&ip_int);
    printf("%s\n",ip_int);

    ip_p=(char*)&ip_int;
    printf("%d,%d,%d,%d",*ip_p,*(ip_p+1),*(ip_p+2),*(ip_p+3));
    
}

 运行结果:

inet_ntop()

        功能:整形数据转字符串格式

const char *inet_ntop(int family,const void *addrptr,char *strptr,size_t len)

参数:

        family   协议族

        addrptr   32位无符号整数

        strptr      点分十进制数串

        len        strptr缓存区长度

返回值:

        成功:返回字符串首地址

        失败:返回NULL

例子:

#include <apra/inet.h>
int main(){
 
    unsigned char ip_int[]={192,168,3,103};
    char ip_str[16]="";//"192.168.3.103"

    //将整型转为字符串
    inet_ntop(AF_INET,&ip_int,ip_str,16);
    printf("%s\n",ip_str);
    
}

inet_addr()和inet_ntoa()

只能用在IPv4地址的转换

in_addr_t inet_addr(const char* cp);
//将十进制ip地址转换为整型数据

char *inet_ntoa(struct in_addr in);
//将整型数据转换为十进制ip地址

网络接口socket

socket()函数

分类:

SOCK_STREAM:流式套接字,同于TCP

SOCK_DGRAM:数据套接字,同于UDP

SOCK_RAW:原始套接字,对于其他层次协议

UDP网络编程流程

服务器:

        创建套接字socket()

        将服务器的ip地址、端口号与套接字绑定bind()

        接收数据recvfrom()

        发送数据sendto()

客户端:

        创建套接字socket()

        发送数据sendto()

        接收数据recvfrom()

        关闭套接字close()

UDP客户端只需要设置目的ip地址、目的端口

因为客户端的本地ip、本地port是调用sendto的时候linux系统底层自动给客户端分配

创建套接字socket

#include<sys/socket.h>

int socket(int family,int type,int protocol)

功能:

        创建一个同于通信网络的socket

参数:

        family : 协议族(AF_INET/AF_INET6)

        type : 套接字类型(SOCK_STREAM、SOCK_DGRAW、SOCK_RAW)

        protocol : 协议类型(0、IPPROTO_TCP、IPPROTO_UDP)

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

int main(){
    
    //创建一个用于UDP网络编程的套接字
    
    int sockfd;
    if((sockfd=socket(AF_INET,SOCK_DGRAW,0))==-1){
        perror("fail to socket");
        exit(1);
    }

    printf("sockfd=%d\n",sockfd);
}

执行结果:

IPv4套接字

网络编程中经常使用的结构体是sockaddr_in

为了使不同格式的地址能被传入套接字函数,地址必须要强制转换为通用套接字地址结构

原因是因为使用场合不同,需要使用的结构体不同,但是调用的函数相同,所以定义一个通用结构体,当指定场合时,根据要求传入指定的结构体即可。

#include <netinet/in.h>

struct in_addr
{
    in_addr_t s_addr;// ip地址 4字节
}

//常用
struct sockaddr_in
{
    sa_family_t sin_family;// 协议族  2字节
    in_port_t sin_port;// 端口号  2字节
    struct in_addr sin_addr;// 4字节
    char sin_zero[8];// 填充  8字节
}

//通用结构体
struct sockaddr
{
    sa_family_t sa_family;// 2字节
    char sa_data[14];// 14字节
}

使用场合;

在定义源地址与目的地址的时候,选用struct sockaddr_in

struct sockaddr_in my_addr;

当调用编程接口函数,且该函数需要传入地址结构时需要用struct sockaddr 进行强制转换

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

发送数据sendto函数

ssize_t sendto(int sockfd,const void *buf,
               size_t nbytes,int flags,
               const struct sockaddr *to,
               socklen_t addrlen)

参数:              

        sockfd:套接字

        buf:发送数据缓存区

        nbytes:缓冲区大小

        to:目的主机地址结构的指针
        addrlen:to所指的内容长度

例子:

#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet.h> //sockaddr_in
#include <arpa/inet.h> //htos inet_addr
#include <unistd.h> //close
#include <string.h>

#define N 128

int main(int argc,char const *argv[]){
    //./a.out 192.168.3.78 8080
    if(argv < 3){
        fprintf(stderr,"Usage: %s ip port\n",argv[0]);
        exit(1);
    }
    //1 创建套接字
    int sockfd;
    if((sockfd=socket(AF_INET,SOCK_DGRAM,0)==-1)){
        perror("fail to socket");
        exit(1);
    }
    printf("socket=%d\n",sockfd);

    //2 填充服务器网络信息结构体 sockaddr_in
    struct sockaddr_in serveraddr;
    socklen_t addrlen = sizeof(severaddr);

    serveraddr.sin_family = AF_INET; //协议族 Ipv4
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址
    serveraddr.sin_port = htons(atoi(argv[2])); //端口号

    //3 发送数据
    char buf[N]="";
    while(1){
        fgets(buf,N,stdin);
        buf[strlen(buf)-1] = '\0'; //把buf字符串中的\n转化为\0
        if(sendto(sockfd,buf,N,0,(struct sockaddr *)&severaddr,addrlen)==-1){
            perror("fail to sendto");
            exit(1);
        }
    }
    
    //4 关闭套接字文件描述符
    close(sockfd);

    return 0;
}

运行结果:

绑定bind() 函数

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

功能:将本地协议地址与sockfd绑定

参数:

        sockfd:文件描述符,socket返回值

        myaddr:指向特定协议的地址结构指针

                网络信息结构体 sockaddr_in

        addrlen:该地址结构的长度

成功返回0,失败返回-1

//1 创建套接字
    int sockfd;
    if((sockfd=socket(AF_INET,SOCK_DGRAM,0)==-1)){
        perror("fail to socket");
        exit(1);
    }
    printf("socket=%d\n",sockfd);

//2 将服务器的网络信息结构体绑定前进行填充 sockaddr_in
    struct sockaddr_in serveraddr;
    socklen_t addrlen = sizeof(severaddr);

    serveraddr.sin_family = AF_INET; //协议族 Ipv4
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址
    serveraddr.sin_port = htons(atoi(argv[2])); //端口号

//3 将网络信息结构体与套接字进行绑定
    if(bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(severaddr))== -1){
        perror("fail to band");
        exit(1);
    }    

运行结果:

只有输入合法的ip地址与端口号,才能绑定成功

 接收数据revcfrom()数据

ssize_t recvfrom(int sockfd,void* buf,
                 size_t nbypes,int flags,
                 struct sockaddr *from,
                 socklen_t *addrlen);

功能:接收UDP数据,将源地址信息并保存在from指向的结构中

参数:

        sockfd:套接字

        buf:接收数据缓冲区
        nbypes:缓冲区大小

        flags:套接字标志
        from:源地址结构体指针,用于保存数据的来源
        addrlen:from所指的内容长度

注意:

通过from和addrlen参数来存放数据来源信息

from和addrlen可以为NULL,表示不保存数据来源

//4 接收数据
char buf[N]="";
struct sockaddr_in clientaddr;
socklen_t addrlen=sizeof(struct sockaddr_in);
if(recvfrom(sockfd,buf,N,0,(struct sockaddr *)&clientaddr,&addrlen)==-1){
    perror("fail to recvfrom");
    exit(1);
}

//打印客户端的ip地址和端口号
printf("ip:%s,prot:%d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
//打印接受到的数据
prinff("from client:%s\n",buf);

执行结果:

 UDP客户端

UDP客户端只需要设置目的ip地址、目的端口

因为客户端的本地ip、本地port是调用sendto的时候linux系统底层自动给客户端分配

#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet.h> //sockaddr_in
#include <arpa/inet.h> //htos inet_addr
#include <unistd.h> //close
#include <string.h>

#define N 128

int main(int argc,char const *argv[]){
    //./a.out 192.168.3.78 8080
    if(argv < 3){
        fprintf(stderr,"Usage: %s ip port\n",argv[0]);
        exit(1);
    }
    //1 创建套接字
    int sockfd;
    struct sockaddr_in serveraddr;
    socklen_t addrlen = sizeof(severaddr);

    if((sockfd=socket(AF_INET,SOCK_DGRAM,0)==-1)){
        perror("fail to socket");
        exit(1);
    }

    //客户端指定自己的ip地址与端口号,一般不需要,系统自动生成
#if 0
    struct sockaddr_in clientaddr;
    clientaddr.sin_family = AF_INET; //协议族 Ipv4
    clientaddr.sin_addr.s_addr = inet_addr(argv[3]); //客户端的ip地址
    clientaddr.sin_port = htons(atoi(argv[4])); //客户端的端口号
    if(bind(sockfd,(struct sockaddr *)&serveraddr,addrlen)<0){
        perror("fail to band");
        exit(1);
    }
#endif

    //2 填充服务器网络信息结构体 sockaddr_in

    serveraddr.sin_family = AF_INET; //协议族 Ipv4
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址
    serveraddr.sin_port = htons(atoi(argv[2])); //端口号

    //3 进行通信
    char buf[N]="";
    while(1){
        fgets(buf,N,stdin);
        buf[strlen(buf)-1] = '\0'; //把buf字符串中的\n转化为\0

        //向服务器发送数据
        if(sendto(sockfd,buf,N,0,(struct sockaddr *)&severaddr,addrlen)==-1){
            perror("fail to sendto");
            exit(1);
        }
        char text[32]="";
        
        //从服务器接收数据
        if(recvfrom(sockfd,text,sizeof(text),0,(struct sockaddr *)&severaddr,&addrlen)==-1){
            perror("fail to recvfrom");
            exit(1);
        }
        printf("from server:%s\n",text);
    }
    
    //4 关闭套接字文件描述符
    close(sockfd);

    return 0;
}

UDP服务器

服务器需要绑定,是因为它的本地port需要固定,而不是随机的

服务器也可以主动给客户端发送数据

客户端可以bind,这样客户端的本地端口是固定的,但是一般不这样做

#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet.h> //sockaddr_in
#include <arpa/inet.h> //htos inet_addr
#include <unistd.h> //close
#include <string.h>

#define N 128

int main(int argc,char const *argv[]){
    //./a.out 192.168.3.78 8080
    if(argv < 3){
        fprintf(stderr,"Usage: %s ip port\n",argv[0]);
        exit(1);
    }

    int sockfd;
    struct sockaddr_in serveraddr;
    socklen_t addrlen = sizeof(severaddr);

    //1 创建套接字
    if((sockfd=socket(AF_INET,SOCK_DGRAM,0)==-1)){
        perror("fail to socket");
        exit(1);
    }
    printf("socket=%d\n",sockfd);

    //2 填充服务器网络信息结构体

    serveraddr.sin_family = AF_INET; //协议族 Ipv4
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址
    serveraddr.sin_port = htons(atoi(argv[2])); //端口号

    //3 绑定套接字与服务器网络信息结构体
    if(bind(sockfd,(struct sockaddr *)&serveraddr,addrlen)<0){
        perror("fail to band");
        exit(1);
    }

    //4 进行通信
    while(1){
        char text[32]="";
        struct sockaddr_in clientaddr;

        //从客户端接收数据
        if(recvfrom(sockfd,text,sizeof(text),0,(struct sockaddr *)&clientaddr,&addrlen)==-1){
            perror("fail to recvfrom");
            exit(1);
        }
        printf("[%s-%d:%s\n]",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));

        strcat(text,"*_*");//数据处理,追加符号

        //将数据发送给客户端
        if(sendto(sockfd,text,sizeof(text),0,(struct sockaddr *)&clientaddr,addrlen)==-1){
            perror("fail to sendto");
            exit(1);
        }
    }
    
    //4 关闭套接字文件描述符
    close(sockfd);

    return 0;
}

执行结果

 2 TFTP

        TFTP,简单文件传输协议,基于UDP,不进行用户有效性认证,数据传输分为二进制模式、文本模式。

TFTP的通信过程

总结:

服务器在69号端口等待客户端的请求

 服务器若批准次请求,则使用临时端口与客户端进行通信

每个数据包的编号不同,从1开始

每个数据包都要得到ACK的确认如果发现超时,则需要重新发送最后的数据包

数据的长度以512B传输

小于512的数据意味着数据传输结束

TFTP协议包结构

TFTP的通信代码

设置服务器

客户端编写代码

#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet.h> //sockaddr_in
#include <arpa/inet.h> //htos inet_addr
#include <unistd.h> //close
#include <string.h>
#include <sys/stat.h>

#define N 128

void do_dowmload(int sockfd,struct sockaddr_in seversddr){
    char filename[128]="";
    printf("请输入要下载的文件名:");
    scanf("%s",filename);
    
    //给服务器发送消息,告知服务器执行下载操作
    unsigned char text[1024]="";
    int text_len;
    socklen_t addrlen=sizeof(struct sockaddr_in);
    int fd;//文件描述符
    int flags=0;//是否需要创建文件
    int num=0;//数据包编号
    ssize_t bytes;//接收的数据大小
    
    //构建给服务器发送的TFTP指令并发送给服务器,例如01test.txt0octxt0
    text_len=sprintf(text,"%c%c%s%v%s%c",0,1,filename,0,"octet",0);
    if(sendto(sockfd,text,text_len,0,(struct sockaddr *)&serveraddr,addrlen)<0){
        perror("fail to sendto");
        exit(1);
    }
    
    while(1){
        //接收服务器的数据并处理
        if(bytes=recvfrom(sockfd,text,sizeof(text),0,(struct sockaddr *)&serveraddr,&addrlen)<0){
            perror("fail to recvfrom");
            exit(1);
        }

        //判断操作码执行相应的处理
        if(text[1]==5){
            printf("error:%s\n",text+4);
        }else if(text[1]==3){
            if(flags==0){
                if(fd=open(filename,.o_WRONLY|O_CREAT|O_TRUNC,0664)<0){
                    perror("fail to open");
                    exit(1);
                }
                flags=1;
            }
            //对比编号和接收的数据大小并将其写入文件中
            if(num+1==ntohs(*(unsigned short *)(text+2))&&(bytes==512)){
                num=ntohs(*(unsigned short *)(text+2));
                if(write(fd,text+4,bytes-4)<0){
                    perror("fail to write");
                    exit(1);
                }
            
                //当文件写入完毕后,给服务器发送一个ACK
                text[1]=4;
                if(sendto(sockfd,text,4,0,(struct sockaddr *)&serveraddr,addrlen)<0){
                    perror("fail to sendto");
                    exit(1);
                }
        
            }
            //当最后一个数据被接受完毕后,写入文件后退出函数
            else if(num+1==ntohs(*(unsigned short *)(text+2))&&(bytes<512)){
                 if(write(fd,text+4,bytes-4)<0){
                    perror("fail to write");
                    exit(1);
                }
            
                text[1]=4;
                if(sendto(sockfd,text,4,0,(struct sockaddr *)&serveraddr,addrlen)<0){
                    perror("fail to sendto");
                    exit(1);
                }
                printf("文件下载完毕\n");
                return;
         
            }
        }
    }
}
int main(int argc,char const *argv[]){
    //./a.out 192.168.3.78 8080
    if(argv < 3){
        fprintf(stderr,"Usage: %s ip port\n",argv[0]);
        exit(1);
    }

    int sockfd;
    struct sockaddr_in serveraddr;

    //1 创建套接字
    if((sockfd=socket(AF_INET,SOCK_DGRAM,0)==-1)){
        perror("fail to socket");
        exit(1);
    }

    //2 填充服务器网络信息结构体

    serveraddr.sin_family = AF_INET; //协议族 Ipv4
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //tftp服务器的ip地址
    serveraddr.sin_port = htons(atoi(69)); //tftp服务器的端口号固定是69

    //3 进行下载操作
    do_download(sockfd,serveraddr);

    return 0;
}

执行结果:

 3 UDP广播、多播

广播

广播:由一台主机向该主机所在的子网内所有主机发送数据的方式,广播只能用UDP,不能用TCP实现。

广播的应用:

        1、地址解析协议(ARP)

        2、动态主机配置协议(DHCP)

        3、网络时间协议(NTP)

广播实例

 客户端:

#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet.h> //sockaddr_in
#include <arpa/inet.h> //htos inet_addr
#include <unistd.h> //close
#include <string.h>

#define N 128

int main(int argc,char const *argv[]){
    //./a.out 192.168.3.78 8080
    if(argv < 3){
        fprintf(stderr,"Usage: %s ip port\n",argv[0]);
        exit(1);
    }

    int sockfd;
    struct sockaddr_in broadcataddr;
    socklen_t addrlen = sizeof(broadcataddr);

    //1 创建套接字
    if((sockfd=socket(AF_INET,SOCK_DGRAM,0)==-1)){
        perror("fail to socket");
        exit(1);
    }

    //2 设置为允许发送广播权限
    int on=1;
    if(setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on))<0){
        perror("fail to setsockopt");
        exit(1);
    }
    
    
    //3 填充服务器网络信息结构体

    broadcataddr.sin_family = AF_INET; //协议族 Ipv4
    broadcataddr.sin_addr.s_addr = inet_addr(argv[1]); //192.168.3.255   255.255.255.255
    broadcataddr.sin_port = htons(atoi(argv[2])); //端口号保持一致


    //4 进行通信
    char buf[128]="";
    while(1){
        fgets(buf,sizeof(buf),stdin);
        buf[strlen(buf)-1]='\0';
        if(sendto(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&broadcataddr,addrlen)==-1){
            perror("fail to sendto");
            exit(1);
        }
        
    }
    
    //4 关闭套接字文件描述符
    close(sockfd);

    return 0;
}

服务器端:

#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <sys/types.h>
#include <sys/socket.h> //socket
#include <netinet.h> //sockaddr_in
#include <arpa/inet.h> //htos inet_addr
#include <unistd.h> //close
#include <string.h>

#define N 128

int main(int argc,char const *argv[]){
    //./a.out 192.168.3.78 8080
    if(argv < 3){
        fprintf(stderr,"Usage: %s ip port\n",argv[0]);
        exit(1);
    }

    int sockfd;
    struct sockaddr_in broadcataddr;
    socklen_t addrlen = sizeof(broadcataddr);

    //1 创建套接字
    if((sockfd=socket(AF_INET,SOCK_DGRAM,0)==-1)){
        perror("fail to socket");
        exit(1);
    }

    //2 填充服务器网络信息结构体

    broadcataddr.sin_family = AF_INET; //协议族 Ipv4
    broadcataddr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址
    broadcataddr.sin_port = htons(atoi(argv[2])); //端口号

    //3 绑定套接字与服务器网络信息结构体
    if(bind(sockfd,(struct sockaddr *)&broadcataddr,addrlen)<0){
        perror("fail to band");
        exit(1);
    }

    //4 进行通信
    char text[32]="";
    struct sockaddr_in sendaddr;

    while(1){

        //从客户端接收数据
        if(recvfrom(sockfd,text,sizeof(text),0,(struct sockaddr *)&sendaddr,&addrlen)==-1){
            perror("fail to recvfrom");
            exit(1);
        }
        //打印
        printf("[%s-%d:%s\n]",inet_ntoa(sendaddr.sin_addr),ntohs(sendaddr.sin_port));

    }

    return 0;
}

运行结果:

UDP多播

多播:数据的收发仅仅在同一分组中进行,多播也成为组播

多播的特点:

        1、多播地址表示一组接口

        2、多播可以用于广域网使用

        3、在IPv4中多播是可选的 

多播地址范围:224.0.0.1--239.255.255.254

                        十六进制:E0.00.00.01-EF.FF.FF.FF

多播的工作过程

比起广播,多播具有可控性,只有加入多播组才会接收数据

 多播流程

发送者:

        1 创建套接字socket()

        2 向多播地址发送数据sendto()

接收者:

        1 创建套接字socket()

        2 设置为加入多播组setsockopt()

        3 将套接字与多播信息结构体绑定bind()

        4 接收数据

多播地址结构体

 多播套接口选项

int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t optlen);
//成功返回0,失败返回-1

 加入多播组实例:

 4 TCP网络编程

TCP与UDP的区别

TCP流程

 服务器:

        1 创建套接字socket()

        2 将套接字与服务器结构体绑定bind()

        3 将套接字设置为监听状态listen()

        4 阻塞等待客服端的连接请求accept()

        5 进行通信recv()、send()

        6 关闭套接字close()

客户端:

        1 创建套接字socket()

        2 发送客户端连接请求connect()

        3 进行通信send()、recv()

        4 关闭套接字close()

socket()

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

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

参数:

        domain:协议族(AF_UNIX、AF_INET、AF_INET6)

        types:套接字类型(SOCK_STEAM、SOCK_DGRAM、SOCK_RAM)

        protocol:附加协议,不需要时设置为0

返回值:

        成功:文件描述符

        失败:-1

实例:

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

int main(int argc,char const *argc[]){
    //通过socket创建一个TCP套接字
    int sockfd;
    if(sockfd=socket(AF_INET,SOCK_STREAM,0)==-1){
        perror("fail to socket");
        exit(1);
    }
    printf("sockfd=%d\n",sockfd);
    return 0;
}

执行结果:

connect()

#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *addr,socklen_t len);

参数:

        sockfd:套接字

        addr:连接的服务器地址结构

        len:地址结构长度

返回值:

        成功:0

        失败:其他

send()

#include <sys/socket.h>
ssize_t send(int sockfd,const void *buf,size_t nbytes,int flags);

 参数:

        sockfd:套接字

        buf:发送数据的地址

        nbytes:发送缓数据的大小(以字节为单位)

        flags:套接字标志(0:阻塞、MSG_DONTWAIT:非阻塞)

返回值:

        成功:发送的字节数

        失败:-1

注意:TCP不能发送长度为0的数据包

recv()

#include <sys/socket.h>
ssize_t recv(int sockfd,void *buf,size_t nbytes,int flags);

 参数:

        sockfd:套接字

        buf:接收数据的缓冲区的地址

        nbytes:接收缓区的大小(以字节为单位)

        flags:套接字标志(0:阻塞、MSG_DONTWAIT:非阻塞)

返回值:

        成功:发送的字节数

        失败:-1

        如果发送端关闭文件描述符或者关闭进程,则recv函数会返回0

TCP客户端

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

#define N 128

int main(int argc,char const *argv[]){
    if(argc<3){
        fprintf(stderr,"Usage:%s [ip] [port]\n",argv[0]);
        exit(1);
    }
    
//1 创建套接字
    int sockfd;
    if(sockfd=socket(AF_INET,SOCK_STEARM,0)==-1){
        perror("fail to socket");
        exit(1);
    }

//2 发送客户端连接请求
    struct sockaddr_in serveraddr;
    socklen_t addrlen=sizeof(serveraddr);

    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr=inet(argv[1]);
    serveraddr.sin_port=htons(atoi(argv[2]));

    if(connect(sockdfd,(struct sockaddr *)&serveraddr,addrlen)==-1){
        perror("fail to connect");
        exit(1);
    }

//3 进行通信
    //发送数据
    char buf[N]="";
    fgets(buf,N,stdin);
    buf[strlen(buf)-1]='\0';
    if(send(sockdfd,buf,N,0)==-1){
        perror("fail to send");
        exit(1);
    }
    
    //接收数据
    char text[N]="";
    if(recv(sockdfd,text,N,0)==-1){
        perror("fail to recv");
        exit(1);
    }
    //打印接收到的数据
    printf("from sever: %s\n",text);

//4 关闭套接字
    close(sockfd);
}

bind()

#include <sys/types.h>
#include <sys/socket/h>
//将套接字与网络信息结构体进行绑定
int bind(int socket,const struct sockaddr *addr,socklen_t addrlen);

参数:

        sockfd:套接字

        addr:连接的服务器地址结构

                通用结构体:struct sockaddr

                网络信息结构体:sockaddr_in

                #include <netinet/in.h>

        addrlen:地址结构长度

返回值:

        成功:0

        失败:-1

listen()

#include <sys/socket.h>
//将套接字由主动改为被动
//使os为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接
int listen(int sockfd,int backlog);

参数:

        sockfd:套接字

        backlog:连接队列的长度 

返回值:

        成功:0

        失败:其他

accept()

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

参数:

        sockfd:套接字

        addr:连接的服务器地址结构

        addrlen:地址结构长度

返回值:

        成功:新的文件描述符

        失败:-1

TCP服务器

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

#define N 128

int main(int argc,char const *argv[]){
    if(argc<3){
        fprintf(stderr,"Usage:%s [ip] [port]\n",argv[0]);
        exit(1);
    }
    
//1 创建套接字
    int sockfd;
    if(sockfd=socket(AF_INET,SOCK_STEARM,0)==-1){
        perror("fail to socket");
        exit(1);
    }

//2 将套接字与服务器网络信息结构体绑定
    struct sockaddr_in serveraddr;
    socklen_t addrlen=sizeof(serveraddr);

    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr=inet(argv[1]);
    serveraddr.sin_port=htons(atoi(argv[2]));

    if(bind(sockdfd,(struct sockaddr *)&serveraddr,addrlen)==-1){
        perror("fail to bind");
        exit(1);
    }

//3 将套接字设置为被动监听状态
    if(listen(sockfd,10)==-1){
        perror("fail to listen");
        exit(1);
    }

//4 阻塞等待客户端的连接请求
    int acceptfd;
    struct sockaddr_in clientaddr;
    if(acceptfd=accept(sockfd,(struct sockaddr *)&clientaddr,&addrlen)==-1){
        perror("fail to accept");
        exit(1);
    }                    
      printf("ip:%s,port:%d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
    
//5 进行通信
    //tcp服务器与客户端通信时,需要使用accept函数的返回值
    //接收数据
    char buf[N]="";
    if(recv(acceptfd,buf,N,0)==-1){
        perror("fail to recv");
        exit(1);
    }
    //打印数据
    printf("from sever: %s\n",buf);
    strcat(buf," *_* ");

    //发送数据
    if(send(accpetfd,buf,N,0)==-1){
        perror("fail to send");
        exit(1);
    }
   

//6 关闭套接字
    close(sockfd);
    close(acceptfd);
}

执行结果:

 close()关闭套接字

1、关闭一个套接字将导致另一端接收到一个长度为0的数据包

2、服务器:

        关闭套接字将导致服务器无法接受新的连接,但不会影响已经建立的连接

        关闭accept返回的已连接套接字将导致它所代表的连接被关闭,但不会影响服务器的监听

3、客户端:

        关闭连接就是关闭连接,不意味其他。

三次握手

四次挥手

TCP并发服务器

TCP原本不是并发服务器,TCP服务器同一时间只能与一个客户端进行通信。

原因:

        由于TCP服务器有两个读阻塞函数,accept和recv,两个函数需要先后运行,所以导致运行一个函数的时候另一个函数无法执行,所以无法保证一边连接客户端,一边与其他客户端进行通信。

多进程实现并发

int sockfd=socket()
bind()
listen()
while(1){
    acceptfd=accept()
    //用fork函数创建子进程,父进程继续负责连接,子进程负责与客户端进行通信
    pid=fork()
    if(pid>0){
        //父进程执行accept,所有if结束后继续在accept处阻塞
    }else if(pid==0){
        //子进程负责与客户端通信
        while(1){
            recv()/send()
        }
    }
}

案例:

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

#define N 128
#define ERR_LOG(errmsg) do{\
                            perror(errmsg);\
                            exit(1);\
                          }while(0)
//信号处理函数
void handler(int sig){
    wait(NULL);
}

int main(int argc,char const *argv[]){
    if(argc<3){
        fprintf(stderr,"Usage:%s [ip] [port]\n",argv[0]);
        exit(1);
    }
    
//1 创建套接字
    int sockfd;
    if(sockfd=socket(AF_INET,SOCK_STEARM,0)==-1){
        ERR_LOG("fail to socket");
    }
//将套接字设置为允许重复使用本地地址或者设置为端口复用(可选)
    int on=1;
    if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0){
        ERR_LOG("fail to setsockopt");
    }
//2 将套接字与服务器网络信息结构体绑定
    struct sockaddr_in serveraddr;
    socklen_t addrlen=sizeof(serveraddr);

    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr=inet(argv[1]);
    serveraddr.sin_port=htons(atoi(argv[2]));

//3 将套接字与网络信息结构体进行绑定
    if(bind(sockdfd,(struct sockaddr *)&serveraddr,addrlen)==-1){
        ERR_LOG("fail to bind");
    }

//4 将套接字设置为被动监听状态
    if(listen(sockfd,10)==-1){
        ERR_LOG("fail to listen");
    }

    //使用信号,异步方式处理僵尸进程
    signal(SIGCHLD,handler);//当子进程状态改变,就会发出信号,调用信号处理函数

    while(1){
        //5 阻塞等待客户端的连接请求
        int acceptfd;
        struct sockaddr_in clientaddr;
        if(acceptfd=accept(sockfd,(struct sockaddr *)&clientaddr,&addrlen)==-1){
            ERR_LOG("fail to accept");
        }                    
          printf("ip:%s,port:%d\n",inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
        
        pid_t pid;
        if((pid=fork())==-1){ //父进程负责执行accept
            
        }else { //子进程负责与客户端进行通信
            //接收数据
            char buf[N]="";
            ssize_t bytes;
            while(1){
                if((bytes=revc(acceptfd,buf,N,0))<0){
                    ERR_LOG("fail to recv");
                }else if(bytes==0){
                    printf("The client quited\n");//若接收到长度为0的数据,说明此时客户端已经关闭
                    exit(0);
                }
                
                if(strncmp(buf,"quit",4)==0){
                    exit(0);
                }

                printf("from cilent:%s\n",buf);
                
                strcat(buf," ^_^ ");

                if(send(acceptfd,buf,N,0)<0){
                    ERR_LOG("fail to send");
                }
            }     
        }        
    }
    return 0;
}

执行结果:

多线程实现并发

void *thread_fun(void *arg){
    while(1){
        recv()/send()
    }
}

int sockfd=socket()
bind()
listen()
while(1){
    accept()
    //只要有客户端连接上,则创建一个子线程与之通信
    pthread_create(&thread,NULL,thread_fun,)
    pthread_detach();
}

案例:

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

#define N 128
#define ERR_LOG(errmsg) do{\
                            perror(errmsg);\
                            exit(1);\
                          }while(0)
typedef struct{
    stuct sockaddr_in addr;
    int acceptfd;
}MSG;

void *pthread_fun(void *arg){
    char buf[N]="";
    ssize_t bytes;
    MSG msg=*(MSG *)arg;
    while(1){
          if((bytes=revc(acceptfd,buf,N,0))<0){
                ERR_LOG("fail to recv");
          }else if(bytes==0){
                printf("The client quited\n");//若接收到长度为0的数据,说明此时客户端已经关闭
                pthread_exit(NULL);
          }
                
          if(strncmp(buf,"quit",4)==0){
                pthread_exit(NULL);
          }

          printf("from cilent:%s\n",buf);
                
          strcat(buf," ^_^ ");

          if(send(acceptfd,buf,N,0)<0){
                ERR_LOG("fail to send");
          }
      }     

}

int main(int argc,char const *argv[]){
    if(argc<3){
        fprintf(stderr,"Usage:%s [ip] [port]\n",argv[0]);
        exit(1);
    }
    
//1 创建套接字
    int sockfd;
    if(sockfd=socket(AF_INET,SOCK_STEARM,0)==-1){
        ERR_LOG("fail to socket");
    }

//将套接字设置为允许重复使用本地地址或者设置为端口复用(可选)
    int on=1;
    if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0){
        ERR_LOG("fail to setsockopt");
    }

//2 填充网络信息结构体
    struct sockaddr_in serveraddr;
    socklen_t addrlen=sizeof(serveraddr);

    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr=inet(argv[1]);
    serveraddr.sin_port=htons(atoi(argv[2]));

//3 将套接字与网络信息结构体进行绑定
    if(bind(sockdfd,(struct sockaddr *)&serveraddr,addrlen)==-1){
        ERR_LOG("fail to bind");
    }

//4 将套接字设置为被动监听状态
    if(listen(sockfd,10)==-1){
        ERR_LOG("fail to listen");
    }

    int acceptfd;
    struct sockaddr_in clientaddr;

    while(1){
        //5 阻塞等待客户端的连接请求
        if((acceptfd=accept(sockfd,(struct sockaddr *)&clientaddr,&addrlen)<0){        ERR_LOG("fail to accept");
        }
    }

    //创建子线程与客户端通信
    MSG msg;
    msg.addr=clientaddr;
    msg.acceptfd=acceptfd;
    pthread_t thread;
    if(pthread_creat(&thread,NULL,pthread_fun,&msg)!=0){
         ERR_LOG("fail to pthread_create");
    }
    pthread_deatch(thread);

    return 0;
}

 Web服务器

HTTP协议

        超文本协议:浏览器与万维网服务器之间的通信规则

支持C/S架构

简单快速:客户端向服务器请求服务时,值需传送请求方法和路径,常用方法:GET、POST

无连接:限制每次连接诶只处理一个请求

无状态:如果后续需要处理前面的信息,就需要重传

Websever 通信过程

Web 编程开发

网页浏览(GET方式)

浏览器输入:

服务器收到的数据

 

服务器的应答格式:

如果GET/之后的网页存在就发送,否则返回失败

请求成功:

请求失败:

案例:

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

#define N 128
#define ERR_LOG(errmsg) do{\
                            perror(errmsg);\
                            exit(1);\
                          }while(0)
typedef struct{
    stuct sockaddr_in addr;
    int acceptfd;
}MSG;

void *pthread_fun(void *arg){
    int acceptfd=*(int *)arg;
    char buf[N]="";
    char head[]="HTTP/1.1 200 OK\r\n"   \
                "Connect_Type:text/html\r\n"   \
                "\r\n";
    char err[]="HTTP/1.1 404 Not Found\r\n"    \
                "Connect_Type:text/html\r\n"   \
                "\r\n"                        \
                "<HTTP><BODY>File not found</BODY></HTTP>";

    //接收浏览器通过http发送的数据包
    if(recv(acceptfd,buf,N,0)<0){
        ERR_LOG("fail to recv");
    }

    printf("%s\n",buf);


    //通过获取的数据包得到浏览器要访问的网页文件名
    //GET /about.html http/1.1
    char filename[128]="";
    //sscanf函数与空格结束,可以直接截取文件名
    sscanf(buf,"GET /%s",filename);
    
    if(strncmp(filename,"HTTP/1.1",strlen("HTTP/1.1"))==0){
        strcpy(filename,"about.html");
    }

    char path[128]="./sqlite/";
    strcat(path,filename);

    //通过解析出来的网络文件名,查找本地的这个文件
    int fd;
    if((fd=open(path,O_RDONLY))<0){
        //如果文件不存在,则发送对应的err指令
        if(error==ENOENT){
            if(send(acceptfd,err,strlen(err),0)<0){
                ERR_LOG("fail to send");
            }
            close(acceptfd);
            phread_exit(NULL);
        }else{
            ERR_LOG("fail to open");
        }
    }

    //若文件存在,先发送head指令告知浏览器
    if(send(acceptfd,head,strlen(head),0)<0){
        ERR_LOG("fail to send");
    }
    
    //读取网页文件中的内容并发送给浏览器
    ssize_t bytes;
    char text[1024];
    while((bytes=read(fd,text,1024))>0){
        if(send(acceptfd,text,bytes,0)>0){
            ERR_LOG("fail to send");
        }
    }

    pthread_exit(NULL);

}

int main(int argc,char const *argv[]){
    if(argc<3){
        fprintf(stderr,"Usage:%s [ip] [port]\n",argv[0]);
        exit(1);
    }
    
//1 创建套接字
    int sockfd;
    if(sockfd=socket(AF_INET,SOCK_STEARM,0)==-1){
        ERR_LOG("fail to socket");
    }

//将套接字设置为允许重复使用本地地址或者设置为端口复用(可选)
    int on=1;
    if(setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0){
        ERR_LOG("fail to setsockopt");
    }

//2 填充网络信息结构体
    struct sockaddr_in serveraddr;
    socklen_t addrlen=sizeof(serveraddr);

    serveraddr.sin_family=AF_INET;
    serveraddr.sin_addr.s_addr=inet(argv[1]);
    serveraddr.sin_port=htons(atoi(argv[2]));

//3 将套接字与网络信息结构体进行绑定
    if(bind(sockdfd,(struct sockaddr *)&serveraddr,addrlen)==-1){
        ERR_LOG("fail to bind");
    }

//4 将套接字设置为被动监听状态
    if(listen(sockfd,10)==-1){
        ERR_LOG("fail to listen");
    }

    int acceptfd;
    struct sockaddr_in clientaddr;

    while(1){
        //5 阻塞等待客户端的连接请求
        if((acceptfd=accept(sockfd,(struct sockaddr *)&clientaddr,&addrlen)<0){        ERR_LOG("fail to accept");
        }
    
        printf("%s--%d\n",inet_ntoa(cilentaddr.sin_addr),ntohs(cilentaddr.sin_port));

        //创建子线程与客户端通信
        pthread_t thread;
        if(pthread_creat(&thread,NULL,pthread_fun,&acceptfd)!=0){
             ERR_LOG("fail to pthread_create");
        }
        pthread_deatch(thread);
    }

    return 0;

}

 网络通信过程

交换机

 路由器

 UDP与TCP回顾

UDP

 client

1、创建socket接口

2、定义sockaddr_in变量,其中ip、port为目的主机的信息

3、可发送0长度的数据包

server

1、bind本地主机的ip、port等信息

2、接收到的数据包中包含来源主机的ip、port信息

TCP 

client

1、connect 来建立连接

2、write、read 收发数据

3、不可发送0长度的数据

server

1、bind 本地主机的ip、port 等信息

2、listen 把主动套接字变为被动

3、accept 会有新的返回值

4、多进程、线程完成并发

原始套接字 (SOCK_RAW)

 原始套接字(SOCK_RAW)

1、一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心

2、可以接收本机网卡上所有的数据帧(数据包)对于监听网络流量和分析网络数据很有作 3、开发人员可发送自己组装的数据包到网络上

4、广泛应用于高级网络编程

5、网络专家、黑客通常会用此来编写奇特的网络程序

 原始套接字可以收发

        1、内核没有处理的数据包,因此要访问其他协议

        2、发送的数据需要使用,原始套接字(SOCK_RAW)

#include <netinet.ether.h>

int socket(PF_PACKET,SOCK_RAW,protocol)

参数:

protocol:指定可以接收或发送的数据包类型

        ETH_P_IP:IPv4数据包

        ETH_P_ARP:ARP数据包

        ETH_P_ALL:所有数据包

返回值:

         成功:文件描述符

         失败:-1

#include <sys/socket.h>
#include <sys/types.h> //socket
#include <stdio.h> //printf
#include <stdlib.h> //exit
#include <netinet/ether.h> //ETH_P_ALL
#include <unistd.h> //close
#include <arpa/inet.h> //htons

int main(int agrc,char const *argv[]){
    //创建链路层原始套接字
    int sockfd;
    if((sockfd=socket(AF_PACKET,SOCK_RAW,htons(ETH_P_ALL)))<0){
        pritnf("fail to socket");
        exit(1);
    }
    printf("sockfd=%d\n",sockfd);
    
    //关闭套接字
    close(sockfd);
}

需要用管理员权限运行(sudo)

UDP数据包

1.源端口号:发送方端口号

2.目的端口号:接收方端口号

3. 长度:UDP用户数据报的长度,最小值是8(仅有首部),需要包括数据的长度

4.校验和:检测UDP用户数据报在传输中是否有错,有错就丢弃

 IP数据报

1.版本:IP协议的版本。通信双方使用过的IP协议的版本必须一致,目前最广泛使用的IP协议版本号为 4(即IPv4)

2.首部长度:单位是32位(4字节)

3.服务类型:一般不适用,取值为0。前3位:优先级,第4-7位:延时,吞吐量,可靠性,花费。第8位保留

4.总长度:指首部加上数据的总长度,单位为字节。最大长度为65535字节。

5.标识 (identification):用来标识主机发送的每一份数据报。IP软件在存储器中维持一个计数器,每产生一个数据报,计数器就加1,并将此值赋给标识字段。

6.标志(flag):目前只有两位有意义。 标志字段中的最低位记为MF。MF=1即表示后面“还有分片”的数据报。MF=0表示这已是若干数据报片中的最后一个。 个 标志字段中间的一位记为DF,意思是“不能分片”,只有当DF=0时才允许分片

7.片偏移:指出较长的分组在分片后.某片在源分组中的相对位置,也就是说,相对于用户数据段的起点,该片从何处开始。片偏移以8字节为偏移单位。

8.生存时间:TTL,表明是数据报在网络中的寿命,即为“跳数限制”,由发出数据报的源点设置这个字段。路由器在转发数据之前就把TTL值减一,当TTL值减为零时,就丢弃这个数据报。通常设置为32、 64、128。

9.协议:指出此数据报携带的数据时使用何种协议,以便使目的主机的卫层知道应将数据部分上交给哪个处理过程,常用的ICMP(1),IGMP(②),TCP(6),UDP(17),IPv6(41)

10.首部校验和:只校验数据报的首部,不包括数据部分。

11.源地址:发送方I地址

12.目的地址:接收方I地址

13.选项:用来定义一些任选项;如记录路径、时间戳等。这些选项很少被使用,同时并不是所有主机

以太网包

 TCP数据包

1.源端口号:发送方端口号

2.目的端口号:接收方端口号

3.序列号:本报文段的数据的第一个字节的序号

4.确认序号:期望收到对方下一个报文段的第一个数据字节的序号

5. 首部长度(数据偏移):TCP报文段的数据起始处距离TCP报文段的起始处有多远,即首部长度。单位:32位,即以4字节为计算单位。

6. 保留:占6位,保留为今后使用,目前应置力0

7.紧急URG:此位置1,表明紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送

8.确认ACK:仅当ACK=1时确认号字段才有效,TCP规定,在连接建立后所有传达的报文段都必须把ACK

9. 推送PSH:当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作,这时,发送方TCP把PSH置1,并立即创建一个报文段发送出去,接收方收到PSH=1的报文段,就尽快地(即“推送”向前)交付给接收应用进程,而不再等到整个缓存都填满后再向上交付

10. 复位RST:用于复位相应的TCP连接

11.同步SYN:仅在三次握手建立TCP连接时有效。当SY=1而ACK=0时,表明这是一个连接请求报文段,对方若同意建立连接,则应在相应的报文段中使用SYN=1和ACK=1.因此,SYN置1就表示这是一个连接请求或连接接受报文

12. 终止PIN:用来释放一个连接。当FIN=1时,表明此报文段的发送方的数据已经发送完毕,并要求释放运输连接。

13. 窗口:指发送本报文段的一方的接收窗口(而不是自己的发送窗口)

14. 校验和:校验和字段检验的范围包括首部和数据两部分,在计算校验和时需要加上12字节的伪头部

 ICMP数据包

Web开发

HTML语言

        超文本标记语言

HTML标签

元信息标记<meta>

用法:

        <meta 属性=值 content=值>

例如:

        <meta name="keyworks"

          content="W2shool">

 文字标签

标题标签

 超链接标签

<a href="http://www.baidu.com">百度一下</a>

<a href="#mypos">回到指定位置</a>

<a href="mypos">回到#mypos位置</a>

<a href="#top">回到顶部</a>

<a href="./04_char.html"><img src="./image/dog/jpg"></a>

表格标签

 

 表单标签

 

 

 

Javascript

网页使用js脚本的三种方式 

直接添加脚本

<input type="button" οnclick="alter('欢迎');" value="点击">

使用script标记插入脚本

  <script type="text/javascript">

                //编写脚本代码

   </script>

链接脚本文件(常用)

<script type="text/javascript"

src="文件名.js"> </script>

 js编程


网站公告

今日签到

点亮在社区的每一天
去签到