编写udp服务器
1.注意要点:
- udp是无连接,不稳定,面向数据报的一种传输层协议;
- 既然他不可靠为什么还要用呢?
- 其一:当应用程序使用广播或多播时只能使用UDP协议;
- 其二:由于他是无连接的,所以速度快
- 如果一方的数据报丢失,那另一方将无限等待,解决办法是设置一个超时重传机制;
- 建立UDP套接口时socket函数的第二个参数应该是SOCK_DGRAM,说明是建立一个UDP套接口;
- 由于UDP是无连接的,所以服务器端并不需要listen或accept函数。
2.udp实现过程图:
由于udp是一种不可靠的传输协议,若出现丢包问题,此时recvfrom函数将会阻塞式等待数据的到来;
- socket函数:为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。
#include <sys/socket.h>
int socket(int family,int type,int protocol);
返回:非负描述字---成功 -1---失败
第一个参数指明了协议簇,目前支持5种协议簇,最常用的有AF_INET(IPv4协议)和AF_INET6(IPv6协议);第二个参数指明套接口类型,有三种类型可选:SOCK_STREAM(字节流套接口)、SOCK_DGRAM(数据报套接口)和SOCK_RAW(原始套接口);如果套接口类型不是原始套接口,那么第三个参数就为0。
- bind函数:为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen);
返回:0---成功 -1---失败
第一个参数是socket函数返回的套接口描述字;第二和第第三个参数分别是一个指向特定于协议的地址结构的指针和该地址结构的长度。
- recvfrom函数:UDP使用recvfrom()函数接收数据,他类似于标准的read(),但是在recvfrom()函数中要指明目的地址。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr * from, size_t *addrlen);
返回接收到数据的长度---成功 -1---失败
前三个参数等同于函数read()的前三个参数,flags参数是传输控制标志。最后两个参数类似于accept的最后两个参数。
- sendto函数:UDP使用sendto()函数发送数据,他类似于标准的write(),但是在sendto()函数中要指明目的地址。
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr * to, int addrlen);
返回发送数据的长度---成功 -1---失败
前三个参数等同于函数read()的前三个参数,flags参数是传输控制标志。参数to指明数据将发往的协议地址
3.应用层实现采用超时重传,实现udp的可靠传输
在规定的时间内,若没有收到sendto的信息,我们就认为数据包丢失,打破recvfrom的阻塞式等待,设置一个闹钟定时器alarm,当收到SIGALRM信号时,程序中断,转去处理中断响应函数,通知客户端进行重传;
alarm函数:
功能:从调用该函数开始,seconds秒之后向进程发送一个SIGALRM信号
仅仅只是这样是不够的!!!
看到这里也许你以为一切都解决了,但是还有一个容易被人忽视的问题:在Linux中,默认处理中断的方式是:
当从中断调用返回时,继续执行被中断的系统调用(用在刚才说的例子上就是:继续redvfrom……)这中默认处理方式大多数时候很有用,但是我们这里就不行了,那这个问题怎么解决呢?要解决这个问题,就不能单纯的用signal函数去设置中断处理程序了,而是要用另一个函数:sigaction
功能:拦截下signum消息,用act所给的方式处理,将原来的处理方式存在oldact(一般oldact设为NULL);
参数:
signum:需要拦截的消息,这里是SIGALRM;
act:处理中断的方式,是一个结构体,后面会介绍这结构体;
oldact:用来存储原来的处理方式,一般为NULL,表示忽略;
结构体:struct sigaction介绍:
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
成员:第一个sa_handler就是中断处理程序的入口,比如:要用alarm程序处理这个中断,就讲此值设为alarm;
sa_mask:表示在中断处理中要屏蔽的中断;
sa_flags:这是很关键的东西~它包含了一些影响中断处理过程方式的标志,具体取值如下:
SA_NOCLDSTOP:这表示如果所处理的中断是SIGCHLD,由于收到其他信号而导致了子进程终止,将不发送SIG_CHLD;
SA_ONESHOT or SA_RESETHAND:sa_handler所指向的中断处理程序只被执行一次,之后将设为默认的中断处理程序;
SA_RESTART:让被处理的系统调用在中断返回后重新执行;
SA_NOMASK or SA_NODEFFER(这就是我们要用的):在中断处理程序执行时,不屏蔽自己的中断信号;在处理此信号未结束前不理会此信号的再次到来。
signal与sigaction函数区别:
signal是重启动函数,超时以后会自动启动已阻塞的函数,而不是中断它的执行,比如recvfrom,给人的感觉就是使用了alarm,程序依然阻塞在了recvfrom上,不往下执行,如果在信号处理函数中使用printf可以看到超时后输出了一条超时信息,然后signal又启动了recvfrom,继续阻塞。。。sigaction可以自己设置是否重启动函数,即上面例子中的alrmact.sa_flags = SA_NOMASK选项,SA_NOMASK为不重启动,中断已阻塞的函数recvfrom,使程序继续往下执行,SA_RESTART为重启动函数,与signal相同,继续阻塞在recvfrom上。。。
4.自己根据逻辑实现代码
udp_server.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>
#include<signal.h>
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock;
//打印输入提示选项
static void* usage(const char* port){
printf("usage: %s [local_ip] [local_port]\n",port);
}
//中断处理函数
void alarm_handler(int sigon)
{
char* m = "please input again\n";
printf("alarm interrupt\n");
//给客户端发送重发消息
sendto(sock,m,strlen(m),0,(struct sockaddr*)&client,len);
}
int main(int argc,char* argv[]){
if(argc!=3){
usage(argv[0]);
return 1;
}
//创建套接字
sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
perror("socket");
exit(1);
}
//将套接字与ip地址和端口号进行绑定
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0){
perror("bind");
exit(2);
}
char buf[1024];
//设置中断处理
struct sigaction alr;
alr.sa_handler = alarm_handler;
alr.sa_flags = SA_NOMASK;
alr.sa_restorer = NULL;
char* msg = "my name is h";
while(1){
//设置闹钟,当超过10s没有收到消息,则认为数据报丢失,发送SIGALRM信号
alarm(10);
//捕获SIGALRM信号
sigaction(SIGALRM,&alr,NULL);
//读取数据
int r = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&client,&len);
if(r<0){
perror("recvfrom");
exit(3);
}else{
buf[r] = 0;
printf("%s\n",buf);
//回送数据
if(sendto(sock,msg,strlen(msg),0,(struct sockaddr*)&client,len)<0){
perror("sendto");
exit(4);
}
alarm(0);//当数据报传输成功时,这是闹钟没有用处了,关闭闹钟
}
}
return 0;
}
客户端
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
static void* usage(const char* port){
printf("usage: %s [local_ip] [local_port]\n",port);
}
int main(int argc,char* argv[]){
if(argc!=3){
usage(argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_DGRAM,0);
if(sock<0){
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));
local.sin_addr.s_addr = inet_addr(argv[1]);
char buf[1024];
char* msg = "hello world";
while(1){
if(sendto(sock,msg,strlen(msg),0,(struct sockaddr*)&local,sizeof(local))<0){
perror("sendto");
exit(2);
}
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
int r = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&tmp,&len);
if(r<0){
perror("recvfrom");
exit(3);
}
buf[r] = 0;
printf("enter echo# %s\n,buf");
}
return 0;
}