610 likes | 764 Views
Linux 系统开发 —— socket 编程. 计算机科学与工程学院 信息安全专业 张柱 制作. 第二章 面向非连接的协议. 主要内容: 面向连接与面向非连接通信的差异。 怎样实现非连接的输入和输出操作? 怎样编写一个数据报服务器? 关于套接口地址的操作。 怎样 编写一个数据报客户?. 2.1 通信的方法 2.1.1 非连接通信的优点. 所谓 面向非连接 的通信,就是在通信之前不建立连接。提供不可靠的数据包协议,相对于面向连接的协议,其有如下的优势: 更加简单。 富有弹性。 高效。 快速。 具备广播的能力。.
E N D
Linux系统开发——socket编程 计算机科学与工程学院 信息安全专业张柱 制作
第二章 面向非连接的协议 主要内容: 面向连接与面向非连接通信的差异。 怎样实现非连接的输入和输出操作? 怎样编写一个数据报服务器? 关于套接口地址的操作。 怎样 编写一个数据报客户?
2.1 通信的方法 2.1.1 非连接通信的优点 所谓面向非连接的通信,就是在通信之前不建立连接。提供不可靠的数据包协议,相对于面向连接的协议,其有如下的优势: • 更加简单。 • 富有弹性。 • 高效。 • 快速。 • 具备广播的能力。
2.1 通信的方法 2.1.2 非连接通信的却点 相对于面向连接的协议,其有如下的不足: • 通信过程的不可靠。 • 多数据报的无序性。 • 消息的尺寸有限制。 可靠性问题是UDP协议无法得到更广泛应用的一个主要原因。引起数据报丢失的原因很多,如中途路由器的数据报校验错误、路由器或接收主机的缓冲区不足等等原因。UDP分组传输的距离越长,发生丢失的可能性就越大。 UDP协议的多数据报无序性,是指对于多数据报的传输,接收端收到的数据顺序可能是无序的。
2.1 通信的方法 2.1.2 非连接通信的却点 对于消息尺寸的限制,理论上,UDP数据报的最大尺寸略小于64KB,实际上,许多UNIX主机只提供32KB的最大尺寸,甚至只有8KB。 对于超出限制的UDP分组,它将被分解为若干较小的IP分组,然后在接收端进行重新组装。 虽然UDP协议是不连接、不可靠的协议,但是有些应用是面向UDP协议,而不是TCP协议的。比如:DNS、NFS和SNMP等等。
2.1 通信的方法 2.1.3 非连接通信的过程调用 通信过程: 客户不与服务器建立连接,而是调用sendto()函数给服务器发送数据报,其中指定目的地址(服务器地址)。 服务器不接受来自客户端的连接,而是调用recvfrom(),等待来自某个客户的数据到达。recvfrom()将与所接收的数据报一道返回客户的协议地址。
2.2 实现数据报的输入和输出 • 在第一章中,使用的read()和write()可以用于在面向连接的协议中,对套接口进行读和写的操作。 • 当接收和发送基于面向非连接协议的数据报时,就要使用另外的一对函数:sendto()和recvfrom()。因为每个消息都可能被发往不同的目的地,能够用来发送数据报的函数必须可以让程序员指定接收者的地址;同样,接收数据报的函数也需要提供一个简单的方法来使得程序员确定发送者的地址。
2.2 实现数据报的输入和输出2.2.1 sendto(2)函数介绍 sendto(2)函数允许程序员写一个数据报,并同时指定接收者的地址。其语法如下: #include <sys/types.h> #include <sys/socket.h> int sendto(int s,const void *msg,int len,unsigned flags, const struct sockaddr *to,int tolen); 调用成功返回实际发送的字节数,失败返回-1,同时设置errno。 参数s是发送者套接口的文件描述符,由先前由socket()函数生成。msg指向需要发送的数据报信息的缓冲区。len欲发送的数据报信息的长度(字节数)。flags发送选项,通常取0。to指向接收者的通用套接口地址。tolen接收者的套接口地址长度。
2.2 实现数据报的输入和输出2.2.1 sendto(2)函数介绍 注意: • 参数to,必须是一个有效的接收者的套接口地址。 • 参数tolen,必须是正确的接收者的套接口地址长度。 • 参数flags,表示发送的方式,通常取值0,表示无特定选项。MSG_OOB表示发送带外数据。其他取值如下:
2.2 实现数据报的输入和输出 2.2.2 recvfrom(2)函数介绍 recvfrom(2)函数使得程序员能够在接收数据的同时,也能得到发送者的地址。其语法如下: #include <sys/types.h> #include <sys/socket.h> int recvfrom(int s,void *buf,int len,unsigned flags, struct sockaddr *from,int *fromlen); 调用成功返回实际接收的字节数。失败返回-1,同时设置errno。 参数s是接收数据报的套接口文件描述符。buf指向接收消息的缓冲区指针。len接收缓冲区的最大长度(字节数)。flags发送选项,通常取0。from指向发送端套接口地址的指针。fromlen指向发送端套接口地址长度的指针。
2.2 实现数据报的输入和输出2.2.2 recvfrom(2)函数介绍 注意: • 缓冲区buf必须足够大,以容纳所接收的数据报,即len参数必须足够大。 • 参数fromlen是一个指向地址结构长度的指针。在调用前,该指针指向from地址结构的最大长度。调用后,返回发送者地址的真正长度。
2.3 关于地址族 2.3.1 套接口地址结构 套接口不一定需要地址。无名套接口,使用socketpair(2)调用生成的一对套接口是互相连接的,但是它们是没有地址的。 • 匿名调用,在两个连接的套接口中,一个连接远端套接口,一个是发出连接的本地套接口。远端套接口必须有地址,而本地套接口可以是无地址,即匿名的。 • 生成地址,地址仅在使用时产生。因为有时需要一个地址进行通信,但是不关心其具体值。而且这个地址仅在通信过程中保持有效,故如果分配固定地址,即浪费资源,又加重了网管员的负担。
2.3 关于地址族 2.3.1 套接口地址结构(续) 1.如下为通用套接口地址结构 #include <sys/socket.h> struct sockaddr{ sa_family_t sa_family; //地址族,无符号整数2字节,取值为AF_* char sa_data[14];//地址数据,14字节 } 2.使用套接口本地地址?提供本地服务使用。本地套接口domain参数使用AF_LOCAL或AF_UNIX。支持抽象套接口名。本地地址结构如下: #include <sys/un.h> struct sockaddr_un{ sa_family_t sun_family; //值为AF_LOCAL或AF_UNIX,2字节 char sun_path[108]; //有效的UNIX路径名。结尾无需’\0’ }
2.3 关于地址族 2.3.1 套接口地址结构(续) • 传统本地地址的命名空间时文件系统的路径名。 • 一个进程可以使用任何有效的路径名来命名本地套接口。 • 套接口“/dev/printer”的实际布局,如右图。 • 在填写地址前,应该将地址结构中的所有字节清0,方法如下: struct sockaddr_un uaddr; memset(&uaddr,0, sizeof uaddr);
2.3 关于地址族 2.3.1 套接口地址结构(续) 例程:清单2.3(生成本地套接口地址,片断) sockaddr_un addr_unix; int len_unix,sck_unix; const char pth_unix[]=“/tmp/my_sock”; //生成套接口 sck_unix=socket (AF_UNIX,SOCK_STREAM,0); ……… unlink(pth_unix);//删除文件描述符pth_unix关联的文件 memset(&addr_unix,0,sizeof addr_unix); //地址结构清0 #include <unistd.h> int unlink(const char *pathname); //用于删除目录项和文件。 #include <string.h> void menset(void *s,int c,size_t n); //用字符c填充缓冲区s的n个字节。
2.3 关于地址族 2.3.1 套接口地址结构(续) 例程:清单2.3(生成本地套接口地址,片断,续) //下面初始化地址结构 addr_unix.sun_family=AF_UNIX;//设置地址族 //设置本地地址 strncpy(addr_unix.sun_path,pth_unix,sizeof addr_unix. sun_path-1)[sizeof addr_unix.sun_path-1]=0;/*strncpy(addr_unix.sun_path,pth_unix,sizeof addr_unix. * sun_path-1); *(addr_unix.sun_path )[sizeof addr_unix.sun_path-1]=0; */ len_unix=SUN_LEN(&addr_unix); //计算地址长度,宏SUN_LEN要求在字符串尾部为空字符。 #include <string.h> char *strncpy(char *dst,const char *src,size_t n); //从源字符串中拷贝n个字符到目标字符串中
2.3 关于地址族 2.3.1 套接口地址结构(续) 例程:(生成本地套接口地址,片断,续) //给套接口绑定地址 z=bind(sck_unix,(struct sockaddr *)&adr_unix,len_unix); if (z==-1) bail("bind()"); //通过调用系统命令netstat完成显示套接口状况 system("netstat -pa |grep af_local”); //关闭使用完毕的套接口 close(sck_unix); unlink(pth_unix); //删除文件描述符pth_unix关联的文件 int bind(int s,struct sockaddr *addr, int addr_len);
2.3 关于地址族 2.3.1 套接口地址结构(续) 小结: 生成一个本地的套接口的基本步骤: • 通过sck_unix=socket()建立套接口; • 使用unlink(pth_unix)删除文件描述符pth_unix关联的文件;因为使用AF_LOCAL和AF_UNIX时,将导致生成本地的文件对象,所以应该保证在使用前后断开该文件描述符和对象的连接。 • 初始化地址结构。 • 给套接口sck_unix绑定地址。 • 套接口建立完毕,可以正常使用。 • 使用完毕后,使用close(sck_unix)调用,关闭套接口。 • 调用unlink(pth_unix)。
2.3 关于地址族 2.3.1 套接口地址结构(续) 生成本地抽象套接口地址。 • 为什么要使用抽象本地地址?因为传统AF_UNIX套接口存在缺陷:总生成一个文件系统对象。无必要,不方便。 • 在Linux内核2.2以上,可以对本地套接口生成抽象地址,从而避免了生成文件系统对象。 • 生成抽象地址的套接口,只需要把路径名的第一个字节设置成空字节,而空字节后的其他部分就是抽象名。 • 使用抽象本地地址的套接口不会生成本地文件系统对象,因此无需使用unlink()调用删除。
2.3 关于地址族 2.3.1 套接口地址结构(续) Z是占位符,后面将其清0 例程:清单2.5(本地抽象套接口地址,片断) const char pth_unix[]=“Z*MY-SOCKET*”; sck_unix=socket (AF_UNIX,SOCK_STREAM,0); ……… memset(&addr_unix,0,sizeof addr_unix); addr_unix.sun_family=AF_UNIX; strncpy(addr_unix.sun_path,pth_unix,sizeof addr_unix. sun_path-1)[sizeof addr_unix.sun_path-1]=0;len_unix=SUN_LEN(&addr_unix); /*使用宏SUN_LEN计算抽象地址长度时,地址的第一个 *字节不能为空字节’\0’,故在完成计算抽象地址长度后,*在将地址的第一个字节清0。*/
2.3 关于地址族 2.3.1 套接口地址结构(续) 例程:(本地抽象套接口地址片断,续) //将路径名的第一个字节清0 addr_unix.sun_path[0]=0; z=bind(sck_unix,(struct sockaddr *)&adr_unix,len_unix); //通过调用系统命令netstat完成显示套接口状况 system("netstat -pa |grep af_unix”); //关闭使用完毕的套接口 close(sck_unix);
2.3 关于地址族 2.3.1 套接口地址结构(续) 小结: • 使用抽象的本地地址,系统不会生成本地文件对象,无需对文件对象进行删除。 • 使用抽象本地地址的要点:将路径名的第一个字节清零,因此在定义路径名时要考虑第一个字符是占位符,并在计算地址长度后,将其清0。 • 先计算地址长度,在将sun_path[0]清0,这是因为计算地址长度的宏SUN_LEN要求第一个字符不为’\0’。 • 程序运行时回显的“path”字段,用“@”表示是一个抽象套接口。
2.3 关于地址族 2.3.1 套接口地址结构(续) 3.internet(IPv4)套接口地址结构: #include <netinet/in.h> struct in_addr{ uint32_t s_addr;//internet地址,32为无符号整数 }; struct sockaddr_in{ sa_family_t sin_family;//地址族,取AF_INET uint16_t sin_port;//TCP/IP端口号,网络字节序 struct in_addr sin_addr;//internet地址,网络字节序 unsigned char sin_zero[8];//占位字节,无需初始化 };
2.3 关于地址族 2.3.2 网络字节序转换 对于多字节数据,不同的CPU有不同的组织方式,其中有两种最基本的字节序: • 小端字节序,一种将低字节存储在起始位置的方法; • 大端字节序,一种将高字节存储在起始位置的方法。 例如,0x1234在两种不同的字节序下的存出情况: Intel的 CPU使用的是小端字节序,Motorola的CPU大都使用大端字节序。因此,如果Motorola的CPU将一个16位的数据通过网络发送出去,而接收端为IntelCPU时,就会出现错误的理解,故需要进行转换。
2.3 关于地址族 2.3.2 网络字节序转换(续) 大小端字节序的转换工作包括两个方向: • 主机字节序到网络字节序; • 网络字节序到主机字节序。 其中,主机字节序是CPU使用的字节序,对于Intel CPU来说,就是小端字节序,对于Motorola CPU来说,就是大端字节序。网络字节序就是大端字节序。 相应的,有以下两类转换函数: • 短(16位)整数转换; • 长(32位)整数转换。
2.3 关于地址族 2.3.2 网络字节序转换(续) 转换函数中, h表示host,n代表net, s代表short, l代表long 转换函数如下: #include <netinet/in.h> unsigned long htonl(unsigned long hostlong); //主机字节序长整数转换成网络字节序长整数(32位) unsigned short htons(unsigned short hostshort); //主机字节序短整数转换成网络字节序短整数(16位) unsigned long ntohl(unsigned long netlong); //网络字节序长整数转换成主机字节序长整数(32位) unsigned short ntohs(unsigned short netshort); //网络字节序短整数转换成主机字节序短整数(16位)
2.3 关于地址族 2.3.2 网络字节序转换(续) 转换函数的使用(例程): short h_short=0x1234; short n_short; //主机字节序短整数转换成网络字节序 n_short=htons(h_short); //网络字节序短整数转换成主机字节序 h_short=ntohs(n_short);
2.3 关于地址族 2.3.2 网络字节序转换(续) 下面的清单(片断)展示了怎样初始化一个具有通用IP地址和通用端口号的AF_INET地址。 struct sockaddr_in addr_inet; int addr_len; memset(&addr_inet,0,sizeof addr_inet); addr_inet.sin_family=AF_INET; addr_inet.sin_port=htons(0); //端口号为0,表示为通用端口号,系统会自动分配本地的端口号 addr_inet.sin_addr.s_addr=htonl(INADDR_ANY); //INADDR_ANY表示为通配地址 addr_len=sizeof addr_inet;
2.3 关于地址族 2.3.3 初始化特定的Internet地址 例程,清单2.9 (片断)。下面的清单(片断)展示了怎样初始化一个具有特定IP地址和端口号的AF_INET地址。 struct sockaddr_in addr_inet; int addr_len; const unsigned char IPno[]={127,0,0,3}; //特定的IP地址,采用网络字节序定义 ……… sck_inet=socket(PF_INET,SOCK_STREAM,0); ……… //下面进行特定地址的初始化,建立套接口地址 memset(&addr_inet,0,sizeof addr_inet); addr_inet.sin_family = AF_INET; addr_inet.sin_port = htons(90000);
2.3 关于地址族 2.3.3 初始化特定的Internet地址 例程,清单2.9 (片断,续) memcpy(&addr_inet.sin_addr.s_addr,IPno,4); //将设置的地址数组复制到特定的结构中 len_inet = sizeof addr_inet; //绑定套接口地址 z=bind(sck_inet,(struct sockaddr *)&addr_inet,len_inet); ……… #include <string.h> void *memcpy(void *dst,const void *src size_t n); //从源缓冲区src中复制n个字节内容到目标缓冲区dst 注意: AF_LOCAL地址的长度是可变的,而AF_INET地址的长度是固定的,16字节。
2.3 关于地址族 2.3.4 理解网络掩码 例程,清单3.1(片断)。用于检查IP地址的类型。 ……… if ((msg & 0x80) ==0x00) { //A类地址 …… }else if ((msg & 0xC0) ==0x80) { //B类地址 …… }else if ((msg & 0xE0) ==0xC0) { //C类地址 …… }else if ((msg & 0xF0) ==0xE0) { //D类地址 …… }else{ //E类地址 …… }
2.3 关于地址族 2.3.5 inet_addr和inet_aton函数 为了减少将字符型IP地址转换成可用的套接口地址的编程负担,系统提供了转换函数。 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> unsigned long inet_addr(const char *string); /*将点分十进制的字符串型IP地址转换成32位二进制网络字节序地址*/ int inet_aton(const char *string,struct in_addr *addr); /*将点分十进制的字符串型IP地址转换成32位二进制网络字节序地址,转换后的地址有addr带回,调用成功返回真,失败返回假,同时设置errno*/
2.3 关于地址族 2.3.5 inet_addr和inet_aton函数(续) 说明: • 参数string为输入的点分十进制的字符型IP地址;addr是一个被新IP地址更新的结构。 • inet_addr(3)函数调用时,如果输入string参数无效,函数返回INADDR_NONE,但是不设置errno。 • inet_addr(3)函数,对输入string参数为255. 255. 255. 255时,返回值仍然时INADDR_NONE,因此,该函数使用受限。 • inet_aton(3)函数是inet_addr(3)函数的改进型。
2.3 关于地址族 2.3.5 inet_addr和inet_aton函数(续) 例程:清单3.3 (片断) 演示如何使用inet_addr和inet_aton struct sockaddr_in addr_inet; sck_inet=socket(PF_INET,SOCK_STREAM,0); ……… memset(&addr_inet,0,sizeof addr_inet); addr_inet.sin_family=AF_INET; addr_inet.sin_port= htons(9000); addr_inet.sin_addr.s_addr=inet_addr("127.0.0.95"); if (addr_inet.sin_addr.s_add==INADDR_NONE) bail("inet_addr); /*if(!inet_aton("127.0.0.95",&addr_inet.sin_addr)) bail("inet_aton"); */ len_inet=sizeof addr_inet; z=bind(sck_inet,(struct sockaddr *)&addr_inet,len_inet); sockaddr_in 的成员类型
2.3 关于地址族 2.3.6inet_ntoa(3)函数 下面的函数用于将套接口中的地址转换成字符串形式的点分十进制IP地址。 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> char *inet_ntoa(struct in_addr addr); 参数是sockaddr结构中成员类型struct in_addr,是输入的套接口地址。返回字符串型的IP地址。 注意: 函数inet_ntoa(3)的返回值在下一次调用前一直有效。因此,如果在线程中调用该函数,要确定每次只有一个线程调用该函数。
2.3 关于地址族 2.3.7inet_network(3)函数 当我们需要提取IP地址中的网络ID或主机ID时,就需要将点分十进制的IP地址转换成主机字节序的32位二进制IP地址。于是有: #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> char *inet_network(const char *addr); 该函数输入的是字符串型的点分十进制IP地址addr,返回主机字节序的32位二进制IP地址。如果输入参数错误,则返回0xFFFFFFFF。 下面的方法可以提取C类IP地址的网络ID和主机ID: unsigned long net_addr,host_addr; net_addr=inet_network(“210.45.151.109”) & 0xFFFFFF00; host_addr=inet_network(“210.45.151.109”) ^ net_addr;
2.3 关于地址族 2.3.8inet_lnaof和inet_netof函数 下面的两个函数分别用于将主机ID和网络ID从套接口地址中提取出来。 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> unsiged long inet_lnaof(struct in_addr addr); //从套接口地址中提取主机ID unsigned long inet_netof(struct in_addr addr); //从套接口地址中提取网络ID 注意: 上述两个函数输入的是网络字节序,计算的结果是根据网络类型来计算的,且都是一个长整数,且是主机序。
2.3 关于地址族 2.3.9inet_makeaddr(3)函数 inet_lnaof和inet_netof函数可以从套接口地址中分别提取IP地址的主机ID和网络ID,inet_makeaddr函数则可以将网络ID和主机ID合并成一个新的套接口地址。 #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> struct in_addr inet_makeaddr(int net,int host); 该函数输入的是数值形式的网络ID和主机ID,这两个数值形式的参数刚好可以有inet_netof和inet_lnaof函数提供。返回网络字节序的套接口地址。
2.3 关于地址族 2.3.10 例程 例程,清单3.7(片断) memset(&addr_inet,0,sizeof addr_inet); addr_inet.sin_family=AF_INET; addr_inet.sin_port=hotns(0); if(!(net_addr=inet_aton(addr[x],&addr_inet.sin_addr))) bail("aton error,bad address!"); host=inet_lnaof(addr_inet.sin_addr); net=inet_netof(addr_inet.sin_addr); ……… addr_inet.sin_addr=inet_makeaddr(net,host); ………
2.4 为套接口绑定地址 2.4.1 bind()调用 当用socket(2)函数创建一个套接口后,该套接口还处于无名状态。如果这个套接口对都在同一个Linux内核中,那么即使没有地址,套接口也可以使用。但是,如果该套接口对,位于两台不同的主机,就必须拥有地址。bind(2)函数就可以为套接口绑定名字,即分配地址。 #include <sys/types.h> #include <sys/socket.h> int bind(int sockfd,struct sockaddr *addr,int addrlen); 该函数调用成功返回0。失败返回-1,同时设置errno。 参数sockfd由socket()函数生成的套接口的描述符;addr分配给套接口的地址;addrlen是套接口地址的长度。 bind()不能为一个已拥有地址的套接口重新绑定地址。另外,参数addr为sockaddr结构指针。
2.4 为套接口绑定地址 2.4.2 获取对端地址 #include <sys/socket.h> int getsockname(int s,struct sockaddr *name,socklen_t *nlen); int getpeername(int s,struct sockaddr *name,socklen_t *nlen); 参数s就是需要知道地址的套接口的描述符;name用于接收地址的缓冲区指针;nlen指向地址缓冲区最大长度的指针,带回地址的实际长度。 • 注意: • 因为该函数适用于任何套接口地址类型,故参数name是通用结构sockaddr指针。 • 参数namelen确定了地址缓冲区的最大字节数,返回时,将被重置为地址的真正长度,故参数namelen不能被赋为常量。
2.4 为套接口绑定地址 2.4.2 获取对端地址(续) • 如果程序员编写某个C函数,需要使用本地套接口描述符作为输入参数,那么,一定要同时传递该套接口的地址,否则该函数无法获得该套接口的地址。 • 有时,程序员不但需要确定本地套接口的地址,还要确定与本地套接口连接的远程套接口的协议地址。 • 函数getsockname(2)可以在不以套接口地址为输入参数时,帮助程序员获得本地套接口的地址。getpeername(2)函数可以获得与本地套接口连接的远程套接口的协议地址。 • getsockname(2)和getpeername(2)函数调用成功,返回0,同时套接口s的地址在参数name中,地址长度在namelen中。失败,返回-1,同时设置errno。
2.4 为套接口绑定地址 2.4.3 例程 实例 清单5.4。绑定地址 sck_inet=socket(PF_INET,SOCK_STREAM,0);//生成套接口 if (sck_inet==-1) bail("socket()");//初始化地址结构 memset(&addr_inet,0,sizeof addr_inet); addr_inet.sin_family=AF_INET; addr_inet.sin_port= htons(9000); if(!inet_aton("127.0.0.95",&addr_inet.sin_addr)){ bail("inet_aton"); } len_inet=sizeof addr_inet; z=bind(sck_inet,(struct sockaddr *)&addr_inet,len_inet); //绑定固定地址 addr_inet.sin_addr.s_addr=htonl(INADDR_ANY); if(addr_inet.sin_addr.s_addr==INADDR_NONE) bail(“bad address”);//初始化通配地址 len_inet=sizeof addr_inet; z=bind(sck_inet,(struct sockaddr *)&addr_inet,len_inet);
2.5 编写UDP数据报服务器程序 简单的UDP数据报服务程序的基本步骤: (1)使用socket(2)函数生成服务器套接口。 (2)建立服务器的地址并用bind(2)函数将其绑定到套接口。 (3)通过调用recvfrom(2)函数等待来自客户端的请求数据报。 (4)处理请求。 (5)将处理的结果返回客户端。 (6)重复执行步骤(3),直到程序结束。
2.5 编写UDP数据报服务器程序 • 如下图,可以看出整个典型的UDP客户端/服务器端程序的调用。客户端不与服务器建立连接,而是只管使用sendto()函数给服务器发送数据报,其中必须作为参数指定目的地址。 • 类似的,服务器端不接受来自客户端的连接,而是只管调用recvfrom(),来等待来自某个客户端的数据到达。recvfrom将与所接收的数据报一道返回客户的协议地址,因此服务器可以把响应发送给正确的客户。 • 注意: • 在UDP协议中,写一个长度为0的数据报是可行的,这将导致发送一个只含一个IP头和一个UDP头而没有数据的IP数据报。问题,其长度为……? • 如果recvfrom函数的from参数是一个空指针,则相应的长度参数fromlen也必须是一个空指针,表示我们不关心数据报的来源。 发送长度为0的数据是可行的,其中仅包括20字节的IP包头个8字节UDP报头。
2.5 编写UDP数据报服务器程序 实例,清单6.1(片断)。编写一个数据报程序,以一定格式的字符串作为输入数据报,然后调用strftime(2)函数生成当前的时间/日期字符串,最后将此结果用另一个数据报返回给客户端程序。 • int main(int argc, char *argv[]){ • if (argc >= 2) srv_addr = argv[1]; • else srv_addr = “127.0.0.23”;//获得服务器地址 • s = socket(PF_INET,SOCK_DGRAM,0);//生成套接口 • ……… • memset(&addr_inet,0,sizeof addr_inet);//初始化地址结构 • addr_inet.sin_family = AF_INET; • addr_inet.sin_port = htons(9090); • if (!inet_aton(srv_addr,&addr_inet.sin_addr)) ……… • len_inet = sizeof addr_inet;
2.5 编写UDP数据报服务器程序 实例,清单6.1(片断,续)。 z = bind(s,(struct sockaddr *)&addr_inet,len_inet); ………//绑定服务器套接口地址 for (; ; ) {//利用无条件循环,等待客户端的请求 len_inet = sizeof addr_clnt; z = recvfrom(s,dgram,sizeof dgram,0,(struct sockaddr *) &addr_clnt,&len_inet); ……… dgram[z] = 0; if (!strcasecmp(dgram,“QUIT”)) break; //验证接收到的数据是否是“QUIT” time(&td);//以秒为单位获得当前时间 tm1 = *localtime(&td);//转换成年月日格式 接收请求的服务器套接口 接收数据的缓冲区 接收数据的缓冲区大小 发送选项 客户端的socket地址 客户端地址长度
2.5 编写UDP数据报服务器程序 //根据输入的格式,格式化日期和时间字符串 strftime(dtfmt,sizeof dtfmt,dgram,&tm1); z = sendto(s,dtfmt,strlen(dtfmt),0,(struct sockaddr *) &addr_clnt,len_inet); ……… } close(s); return 0; } 一个提供年月日的时间结构指针 带回格式化日期的字符串缓冲区。 字符串缓冲区的大小。 规定日期字符串格式的模型字符串。 发送缓冲区大小 发送的数据存放的缓冲区 客户端(接收端)socket地址 客户端socket地址长度 服务器端用于发送数据报的套接口
2.5 编写UDP数据报服务器程序 服务器程序的编译和执行(假设源程序保存为dgramsrv.c): 编译:gcc –ggdb –o dgramsrv –c dgramsrv.c 执行: 1)采用默认的地址,后台运行服务器。 ./dgramsrv & 2)指定地址,后台运行服务器。 ./dgramsrv 210.45.151.109 &