解析C语言基于UDP协议进行Socket编程的要点
两种协议 TCP 和 UDP UDP 这是一个十分简洁的连接方式,假设有两台主机进行通信,一台只发送,一台只接收。 int sock; /* 套接字 */ socklen_t addr_len; /* 发送端的地址长度,用于 recvfrom */ char mess[15]; char get_mess[GET_MAX]; /* 后续版本使用 */ struct sockaddr_in recv_host,send_host; /* 创建套接字 */ sock = socket(PF_INET,SOCK_DGRAM,0); /* 把IP 和 端口号信息绑定在套接字上 */ memset(&recv_host,sizeof(recv_host)); recv_host.sin_family = AF_INET; recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */ recv_host.sin_port = htons(6000); /* 使用6000 端口号 */ bind(sock,(struct sockaddr *)&recv_host,sizeof(recv_host)); /* 进入接收信息的状态 */ recvfrom(sock,mess,15,(struct sockaddr *)&send_host,&addr_len); /* 接收完成,关闭套接字 */ close(sock); 上述代码省略了许多必要的 错误检查 ,在实际编写时要添加 代码解释: int sock; const char* mess = "Hello Server!"; char get_mess[GET_MAX]; /* 后续版本使用 */ struct sockaddr_in recv_host; socklen_t addr_len; /* 创建套接字 */ sock = socket(PF_INET,0); /* 绑定 */ memset(&recv_host,sizeof(recv_host)); recv_host.sin_family = AF_INET; recv_host.sin_addr.s_addr = inet_addr("127.0.0.1"); recv_host.sin_port = htons(6000); /* 发送信息 */ /* 在此处,发送端的IP地址和端口号等各类信息,随着这个函数的调用,自动绑定在了套接字上 */ sendto(sock,strlen(mess),sizeof(recv_host)); /* 完成,关闭 */ close(sock); 上述代码是发送端。 代码解释: /* 前方高度一致,将 bind函数替换为 */ connect(sock,sizeof(recv_host); // 将对方的 IP地址和 端口号信息 注册进UDP的套接字中) while(1) /* 循环的发送和接收信息 */ { size_t read_len = 0; /* 原先使用的 sendto 函数,先择改为使用 write 函数, Windows平台为 send 函数 */ write(sock,strlen(mess)); /* send(sock,0) FOR Windows Platform */ read_len = read(sock,get_mess,GET_MAX-1); /* recv(sock,strlen(mess)-1,0) FOR Windows Platform */ get_mess[read_len-1] = ' '; printf("In Client like Host Recvive From Other Host : %sn",get_mess); } /* 后方高度一致 */ 接收端: /* 前方一致, 添加额外的 struct sockaddr_in send_host; 并添加循环,构造收发的现象*/ while(1) { size_t read_len = 0; char sent_mess[15] = "Hello Sender!"; /* 用于发送的信息 */ sendto(sock,strlen(sent_mess),sizeof(recv_host)); read_len = recvfrom(sock,&addr_len) mess[read_len-1] = ' '; printf("In Sever like Host Recvive From other Host : %sn",mess); } /* 后方高度一致 */ /* * 之所以只在接收端使用 connect 的原因,便在于我们模拟的是 客户端-服务器 的模型,而服务器的各项信息是不会随意变更的 * 但是 客户端就不同了,可能由于 ISP(Internet Server Provider) 的原因,你的IP地址不可能总是固定的,所以只能 * 保证 在客户端 部分注册了 服务器 的各类信息,而不能在 服务器端 注册 客户端 的信息。 * 当然也有例外,例如你就想这个软件作为私密软件,仅供两个人使用, 且你有固定的 IP地址,那么你可以两边都connect,但是 * 一定要注意,只要有一点信息变动,这个软件就可能无法正常的收发信息了。 */ 代码解释 首先要知道,网络通信一般而言是双方的共同进行的,换而言之就是双向的,一个方向只用来发送消息,一个方向只用来读取消息。 /* 假设有一个 TCP 的连接,此为客户端 */ write(sock,"Thank you",10); close(sock); // 写完直接关闭通信 接收端: /* 此为服务器 */ /* 首先关闭写流 */ shutdown(sock_c,SHUT_WR); read(sock_c,GET_MAX); printf("Message : %sn",get_mess); close(sock_c); close(sock_s); // 关闭两个套接字是因为 TCP 服务器端的需要,后续会记录 代码解释 int shutdown(int sock,int howto); sock 代表要操作的套接字
改写一下上方的例子: 接收端: int sock; /* 套接字 */ socklen_t addr_len; /* 发送端的地址长度,用于 recvfrom */ char mess[15]; char get_mess[GET_MAX]; /* 后续版本使用 */ struct sockaddr_in host_v4; /* IPv4 地址 */ struct sockaddr_in6 host_v6; /* IPv6 地址 */ struct addrinfo easy_to_use; /* 用于设定要获取的信息以及如何获取信息 */ struct addrinfo *result; /* 用于存储得到的信息(需要注意内存泄露) */ struct addrinfo * p; /* 准备信息 */ memset(&easy_to_use,sizeof easy_to_use); easy_to_use.ai_family = AF_UNSPEC; /* 告诉接口,我现在还不知道地址类型 */ easy_to_use.ai_flags = AI_PASSIVE; /* 告诉接口,稍后“你”帮我填写我没明确指定的信息 */ easy_to_use.ai_socktype = SOCK_DGRAM; /* UDP 的套接字 */ /* 其余位都为 0 */ /* 使用 getaddrinfo 接口 */ getaddrinfo(NULL,argv[1],&easy_to_use,&result); /* argv[1] 中存放字符串形式的 端口号 */ /* 创建套接字,此处会产生两种写法,但更保险,可靠的写法是如此 */ /* 旧式方法 * sock = socket(PF_INET,0); */ /* 把IP 和 端口号信息绑定在套接字上 */ /* 旧式方法 * memset(&recv_host,sizeof(recv_host)); * recv_host.sin_family = AF_INET; * recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */ * recv_host.sin_port = htons(6000); /* 使用6000 端口号 */ * bind(sock,sizeof(recv_host)); */ for(p = result; p != NULL; p = p->ai_next) /* 该语法需要开启 -std=gnu99 标准*/ { sock = socket(p->ai_family,p->ai_socktype,p->ai_protocol); if(sock == -1) continue; if(bind(sock,p->ai_addr,p->ai_addrlen) == -1) { close(sock); continue; } break; /* 如果能执行到此,证明建立套接字成功,套接字绑定成功,故不必再尝试。 */ } /* 进入接收信息的状态 */ //recvfrom(sock,&addr_len); switch(p->ai_socktype) { case AF_INET : addr_len = sizeof host_v4; recvfrom(sock,(struct sockaddr *)&host_v4,&addr_len); break; case AF_INET6: addr_len = sizeof host_v6 recvfrom(sock,(struct sockaddr *)&host_v6,&addr_len); break; default: break; } freeaddrinfo(result); /* 释放这个空间,由getaddrinfo分配的 */ /* 接收完成,关闭套接字 */ close(sock); 代码解释: 首先解释几个新的结构体 struct addrinfo 这个结构体的内部顺序对于 *nix 和 Windows 稍有不同,以 *nix 为例 struct addrinfo{ int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; struct sockaddr * ai_addr; /* 存放结果地址的地方 */ char * ai_canonname; /* 忽略它吧,很长一段时间你无须关注它 */ struct addrinfo * ai_next; /* 一个域名/IP地址可能解析出多个不同的 IP */ }; ai_family 如果设定为 AF_UNSPEC 那么在调用 getaddrinfo 时,会自动帮你确定,传入的地址是什么类型的
getaddrinfo 强大的接口函数 int getaddrinfo(const char * node,const char * service, 其中,前者是后者的大小上的子集,即一个 struct storage 一定能够装下一个 struct sockaddr_in6,具体(实际上根本看不到有意义的实现) struct sockaddr_in6{ u_int16_t sin6_family; u_int16_t sin6_port; u_int32_t sin6_flowinfo; /* 暂时忽略它 */ struct in6_addr sin6_addr; /* IPv6 的地址存放在此结构体中 */ u_int32_t sin_scope_id; /* 暂时忽略它 */ }; struct in6_addr{ unsigned char s6_addr[16]; } ------------------------------------------------------------ struct sockaddr_storage{ sa_family_t ss_family; /* 地址的种类 */ char __ss_pad1[_SS_PAD1SIZE]; /* 从此处开始,不是实现者几乎是没办法理解 */ int64_t __ss_align; /* 从名字上可以看出大概是为了兼容两个不同 IP 类型而做出的妥协 */ char __ss_pad2[_SS_PAD2SIZE]; /* 隐藏了实际内容,除了 IP 的种类以外,无法直接获取其他的任何信息。 */ /* 在各个*nix 的具体实现中, 可能有不同的实现,例如 `__ss_pad1` , `__ss_pad2`,可能合并成一个 `pad` 。 */ }; 在实际中,我们往往不需要为不同的IP类型声明不同的存储类型,直接使用 struct sockaddr_storage 就可以,使用时直接强制转换类型即可 改写上方 接收端 例子中,进入接收信息的状态部分 /* 首先将多于的变量化简 */ // - struct sockaddr_in host_v4; /* IPv4 地址 */ // - struct sockaddr_in6 host_v6; /* IPv6 地址 struct sockaddr_storage host_ver_any; /* + 任意类型的 IP 地址 */ ... /* 进入接收信息的状态部分 */ recvfrom(sock,(struct sockaddr *)&host_ver_any,&addr_len); /* 像是又回到了只有 IPv4 的年代*/ 补充完整上方对应的 发送端 代码 int sock; const char* mess = "Hello Server!"; char get_mess[GET_MAX]; /* 后续版本使用 */ struct sockaddr_storage recv_host; /* - struct sockaddr_in recv_host; */ struct addrinfo tmp,*result; struct addrinfo *p; socklen_t addr_len; /* 获取对端的信息 */ memset(&tmp,sizeof tmp); tmp.ai_family = AF_UNSPEC; tmp.ai_flags = AI_PASSIVE; tmp.ai_socktype = SOCK_DGRAM; getaddrinfo(argv[1],argv[2],&tmp,&result); /* argv[1] 代表对端的 IP地址, argv[2] 代表对端的 端口号 */ /* 创建套接字 */ for(p = result; p != NULL; p = p->ai_next) { sock = socket(p->ai_family,p->ai_protocol); /* - sock = socket(PF_INET,0); */ if(sock == -1) continue; /* 此处少了绑定 bind 函数,因为作为发送端不需要讲对端的信息 绑定 到创建的套接字上。 */ break; /* 找到就可以退出了,当然也有可能没找到,那么此时 p 的值一定是 NULL */ } if(p == NULL) { /* 错误处理 */ } /* -// 设定对端信息 memset(&recv_host,sizeof(recv_host)); recv_host.sin_family = AF_INET; recv_host.sin_addr.s_addr = inet_addr("127.0.0.1"); recv_host.sin_port = htons(6000); */ /* 发送信息 */ /* 在此处,发送端的IP地址和端口号等各类信息,随着这个函数的调用,自动绑定在了套接字上 */ sendto(sock,p->ai_addrlen); /* 完成,关闭 */ freeaddrinfo(result); /* 实际上这个函数应该在使用完 result 的地方就予以调用 */ close(sock); 到了此处,实际上是开了网络编程的一个初始,解除了现代的 UDP 最简单的用法(甚至还算不上完整的使用),但是确实是进行了交互。 ARP 协议 最简便的方法就是找一个有 WireShark 软件或者 tcpdump 的 *nix 平台,前者你可以选择随意监听一个机器,不多时就能看见 ARP 协议的使用,因为它使用的太频繁了。 ICMP协议 这个协议比较重要。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |