2019年8月19日星期一
一. UDP协议通信
1. UDP协议的特点?
UDP协议是面向于无连接的通信方式,用户只需要知道服务器的IP地址就可以发送数据给服务器,但是数据容易造成丢失。
2. UDP协议服务器过程?
1)创建一个UDP协议的套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
2)绑定IP地址,协议,端口号到套接字上
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(atoi(argv[1]));
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //自动获取主机的IP地址
bind(sockfd,(struct sockaddr *)&srvaddr,len);
3)不断等待对端的连接
recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&Jackaddr,&len);
4)断开套接字
close(sockfd);
3. UDP协议的客户端实现过程?
1)创建一个UDP协议的套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
2)发送数据给服务器
sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&Roseaddr,len);
3)关闭套接字
close(sockfd);
二. 非阻塞IO模型
1. 阻塞IO与非阻塞IO区别?
阻塞 -> 一直等待到有数据为止读取数据。 read(fd);
非阻塞IO -> 先询问有没有数据,如果有,则读取出来,如果没有则马上返回失败。 read(fd);
2. 设置非阻塞IO模式?
由于文件描述符天生是阻塞IO,所以阻塞就不用设置了,但是非阻塞模式需要用户自己添加。
1)建立文件描述符 -> 文件描述符是阻塞。
2)设置非阻塞属性给文件描述符 -> 文件描述符是非阻塞。
3)再调用read()/recv()/read()/recvfrom() -> 非阻塞读取。
3. 如何设置非阻塞IO? -> fcntl() -> man 2 fcntl
使用格式:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fd:需要设置属性的文件描述符
cmd:命令选项 -> 例如: 非阻塞IO模式
F_GETFL -> 获取文件描述符的属性 -> arg参数可以省略!
F_SETFL -> 设置属性到文件描述符中 ->
arg:额外的参数
需要设置的属性,例如: O_NONBLOCK |
返回值:
成功: F_GETFL -> 返回文件的属性 F_SETFL -> 0
失败: -1
例子:给一个文件描述符在原来的基础上添加非阻塞属性
int main(int argc,char *argv[])
{
//1. 先建立文件描述符
int sockfd = socket(AF_INET,SOCK_STREAM,0);
//2. 设置非阻塞属性给文件描述符
int state;
state = fcntl(sockfd,F_GETFL); //state就是这个文件描述符原来的属性
state |= O_NONBLOCK;
fcntl(sockfd,F_SETFL,state);
//3. 接下使用sockfd时就是非阻塞的。
return 0;
}
练习1:使用非阻塞IO读取TCP套接字中的数据。
#include "head.h"
//任务: 负责发送数据给客户端。
void *routine(void *arg) //arg = &connfd
{
int connfd = *(int *)arg;
char buf[50];
while(1)
{
bzero(buf,sizeof(buf));
fgets(buf,50,stdin);
send(connfd,buf,strlen(buf),0);
if(strncmp(buf,"quit",4) == 0)
{
exit(0);
}
}
}
int main(int argc,char *argv[]) // ./server 50001
{
//1. 创建未连接套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
printf("sockfd = %d ",sockfd); //3
//2. 绑定IP地址,端口号等到未连接套接字中
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //协议
srvaddr.sin_port = htons(atoi(argv[1])); //端口号
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //服务器的IP
bind(sockfd,(struct sockaddr *)&srvaddr,len);
//3. 设置监听套接字
listen(sockfd,5);
//4. 等待连接
struct sockaddr_in cliaddr;
int connfd;
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len); //阻塞!
if(connfd > 0)
{
printf("new connection:%s ",(char *)inet_ntoa(cliaddr.sin_addr));
}
//4.4 给已连接套接字设置一个非阻塞的属性
int state;
state = fcntl(connfd,F_GETFL);
state |= O_NONBLOCK;
fcntl(connfd,F_SETFL,state);
//4.5 创建线程
pthread_t tid;
pthread_create(&tid,NULL,routine,(void *)&connfd);
//5. 不断接收客户端的消息
char buf[50];
int ret;
while(1)
{
usleep(500000);
bzero(buf,sizeof(buf));
if(recv(connfd,buf,sizeof(buf),0)>0)
{
printf("from Jack:%s",buf);
}
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
//6. 挂断
close(connfd);
close(sockfd);
return 0;
}
下午练习:
写一个TCP协议服务器,最多可以接收20个客户端的连接,可以随时接收客户端的连接,客户端数据存放数据,只要连接到该服务器的客户端有数据到达,就会将该数据打印在服务器。
三. 多路复用模型
1. 什么是多路复用?
就是先将需要监听的文件描述符加入到一个集合中,然后在规定的时间内/无限等待去监听这个集合。如果在规定的时间内,集合中的文件描述符有数据到达,则其他没有数据到达的文件描述符就会自动被剔除到集合之外,我们用户只需要观察集合中有没有文件描述符在就可以。
2. 如何实现多路复用? -> select() -> man 2 select
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds: 所有正在监测的套接字的最大值加1
readfds: 读就绪的集合 -> 需要监听的集合。
writefds: 写就绪的集合
exceptfds: 异常就绪的集合
timeout: 超时控制
struct timeval {
long tv_sec; -> 秒
long tv_usec; -> 微秒
};
返回值:
成功:就绪文件描述符总数(当超时返回时为0)
失败:-1
关于集合操作函数:
void FD_CLR(int fd, fd_set *set); -> 删除集合中的一个文件描述符
int FD_ISSET(int fd, fd_set *set); -> 测试下文件描述符是否在集合中
返回值:
成功:1 -> 在集合中
失败:0 -> 不在集合中
void FD_SET(int fd, fd_set *set); -> 添加一个文件描述符到集合中
void FD_ZERO(fd_set *set); -> 清空集合中所有的文件描述符
server端:
#include "head.h"
int main(int argc,char *argv[]) // ./server 50001
{
//1. 创建未连接套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
printf("sockfd = %d ",sockfd); //3
//2. 绑定IP地址,端口号等到未连接套接字中
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //协议
srvaddr.sin_port = htons(atoi(argv[1])); //端口号
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //服务器的IP
bind(sockfd,(struct sockaddr *)&srvaddr,len);
//3. 设置监听套接字
listen(sockfd,5);
//4. 等待连接
struct sockaddr_in cliaddr;
int connfd;
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len); //阻塞!
if(connfd > 0)
{
printf("new connection:%s ",(char *)inet_ntoa(cliaddr.sin_addr));
}
//5. 设置一个集合,并将文件描述符加入到集合中
fd_set rset;
int maxfd = connfd > STDIN_FILENO ? connfd : STDIN_FILENO;
char buf[50];
//6. 不断监听集合
while(1)
{
FD_ZERO(&rset);
FD_SET(connfd,&rset);
FD_SET(STDIN_FILENO,&rset);
select(maxfd+1,&rset,NULL,NULL,NULL); //只要有数据到达,就会返回!
if(FD_ISSET(connfd,&rset))
{
bzero(buf,sizeof(buf));
recv(connfd,buf,sizeof(buf),0);
printf("from client:%s",buf);
}
if(FD_ISSET(STDIN_FILENO,&rset))
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
send(connfd,buf,strlen(buf),0);
}
}
close(connfd);
close(sockfd);
return 0;
}
client端:
#include "head.h"
int main(int argc,char *argv[]) //./client 192.168.90.2 50001
{
//1. 创建一个未连接套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
printf("sockfd = %d ",sockfd); //3
//2. 直接发起连接
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET;//协议
srvaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
int ret = connect(sockfd,(struct sockaddr *)&srvaddr,len);
if(ret == -1)
printf("connect error! ");
//5. 设置一个集合,并将文件描述符加入到集合中
fd_set rset;
int maxfd = sockfd > STDIN_FILENO ? sockfd : STDIN_FILENO;
char buf[50];
//6. 不断监听集合
while(1)
{
FD_ZERO(&rset);
FD_SET(sockfd,&rset);
FD_SET(STDIN_FILENO,&rset);
select(maxfd+1,&rset,NULL,NULL,NULL); //只要有数据到达,就会返回!
if(FD_ISSET(sockfd,&rset))
{
bzero(buf,sizeof(buf));
recv(sockfd,buf,sizeof(buf),0);
printf("from client:%s",buf);
}
if(FD_ISSET(STDIN_FILENO,&rset))
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
send(sockfd,buf,strlen(buf),0);
}
}
close(sockfd);
return 0;
}
四. 网络超时接收。
1. 使用多路复用select()函数的最后一个参数。
struct timeval {
long tv_sec; -> 秒
long tv_usec; -> 微秒
};
-> 每次select完,都需要重新设定一个新的时间。
-> 用户可以设置一个时间,在规定的时间的内,没有数据到达,select函数就会返回。
select(maxfd+1,&rset,NULL,NULL,NULL); -> 如果有数据到达,则select()返回!
-> 如果一直都没有数据到达,则select()会无限等待!
struct timeval v;
v.tv_sec = 5;
v.tv_usec = 0;
select(maxfd+1,&rset,NULL,NULL,&v); -> 如果在5秒内,有数据到达,则select函数马上返回
-> 如果在5秒内,没有数据到达,则select函数还是会返回0
例题: 监听键盘与已连接套接字,如果在规定的5秒内,没有数据到达,则打印"timeout!"
#include "head.h"
void *routine(void *arg)
{
int i = 0;
while(1)
{
printf("%d ",i++);
sleep(1);
}
}
int main(int argc,char *argv[]) // ./server 50001
{
//计算时间流逝
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL);
//1. 创建未连接套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
printf("sockfd = %d ",sockfd); //3
//2. 绑定IP地址,端口号等到未连接套接字中
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //协议
srvaddr.sin_port = htons(atoi(argv[1])); //端口号
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //服务器的IP
bind(sockfd,(struct sockaddr *)&srvaddr,len);
//3. 设置监听套接字
listen(sockfd,5);
//4. 等待连接
struct sockaddr_in cliaddr;
int connfd;
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len); //阻塞!
if(connfd > 0)
{
printf("new connection:%s ",(char *)inet_ntoa(cliaddr.sin_addr));
}
//5. 设置一个集合,并将文件描述符加入到集合中
fd_set rset;
struct timeval v;
int maxfd = connfd > STDIN_FILENO ? connfd : STDIN_FILENO;
char buf[50];
int ret;
//6. 不断监听集合
while(1)
{
FD_ZERO(&rset);
FD_SET(connfd,&rset);
FD_SET(STDIN_FILENO,&rset);
bzero(&v,sizeof(v));
v.tv_sec = 5;
v.tv_usec = 0;
ret = select(maxfd+1,&rset,NULL,NULL,&v); //只要有数据到达,就会返回!
if(ret == 0)
printf("timeout! "); //将所有的文件描述符剔除到集合之外!
if(FD_ISSET(connfd,&rset))
{
bzero(buf,sizeof(buf));
recv(connfd,buf,sizeof(buf),0);
printf("from client:%s",buf);
}
if(FD_ISSET(STDIN_FILENO,&rset))
{
bzero(buf,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
send(connfd,buf,strlen(buf),0);
}
}
close(connfd);
close(sockfd);
return 0;
}
2. 设置套接字的属性为超时接收。
1)机制如何?
如果不给套接字设置属性,那么读取这个套接字时就会无限等待!
如果设置了超时属性给套接字,那么读取这个套接字数据时,就会有时间的限制。
2)如何设置属性给套接字? -> 具体用法:setsockopt.txt
setsockopt设置属性函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
sockfd:套接字
level:优先级
SOL_SOCKET:套接字
IPPROTO_IP:IP优先级
IPPRO_TCP:TCP优先级
optname:选项名字 SO_RCVTIMEO
optval:值,使能为1,不使能为0 -> 看最后一栏 struct timeval v
optlen:值类型大小
===========================SOL_SOCKET=======================
optname选项名字 optlen的大小
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获得套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSEADDR 允许重用本地地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int
=========================IPPROTO_IP==========================
IP_HDRINCL 在数据包中包含IP首部 int
IP_OPTINOS IP首部选项 int
IP_TOS 服务类型
IP_TTL 生存时间 int
IP_ADD_MEMBERSHIP 加入组播 struct ip_mreq
=========================IPPRO_TCP===========================
TCP_MAXSEG TCP最大数据段的大小 int
TCP_NODELAY 不使用Nagle算法 int
阻塞情况:
connfd = accept(sockfd);
recv(connfd); -> 一直阻塞,无限等待!
设置套接字属性
connfd = accept(sockfd);
setsockopt(connfd,SOL_SOCKET,SO_RCVTIMEO....); -> connfd本身就多了一个超时的属性
recv(connfd); -> 在规定的时间内,如果没有数据到达,则超时!
#include "head.h"
void *routine(void *arg)
{
int i = 0;
while(1)
{
printf("%d ",i++);
sleep(1);
}
}
int main(int argc,char *argv[]) // ./server 50001
{
//计算时间流逝
pthread_t tid;
pthread_create(&tid,NULL,routine,NULL);
//1. 创建未连接套接字
int sockfd;
sockfd = socket(AF_INET,SOCK_STREAM,0);
printf("sockfd = %d ",sockfd); //3
//2. 绑定IP地址,端口号等到未连接套接字中
struct sockaddr_in srvaddr;
socklen_t len = sizeof(srvaddr);
bzero(&srvaddr,len);
srvaddr.sin_family = AF_INET; //协议
srvaddr.sin_port = htons(atoi(argv[1])); //端口号
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //服务器的IP
bind(sockfd,(struct sockaddr *)&srvaddr,len);
//3. 设置监听套接字
listen(sockfd,5);
//4. 等待连接
struct sockaddr_in cliaddr;
int connfd;
connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len); //阻塞!
if(connfd > 0)
{
printf("new connection:%s ",(char *)inet_ntoa(cliaddr.sin_addr));
}
//5. 设置超时属性给套接字
struct timeval v;
v.tv_sec = 5;
v.tv_usec = 0;
setsockopt(connfd,SOL_SOCKET,SO_RCVTIMEO,&v,sizeof(v));
char buf[50];
int ret;
while(1)
{
bzero(buf,sizeof(buf));
ret = recv(connfd,buf,sizeof(buf),0);
if(ret > 0) //说明在5秒内有数据到达
{
printf("from client:%s",buf);
}
if(ret == -1) //说明已经超时
{
printf("timeout! ");
}
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
//6. 挂断
close(sockfd);
close(connfd);
return 0;
}
五. 广播属性
1. 什么是广播?
广播属于套接字的一种属性,刚创建的套接字不允许广播,所以想使用广播属性,首先必须要先设置广播属性给套接字!
单播 -> 点对点
广播 -> 点对多
2. 广播特点
1)只有UDP协议才能实现广播。
2)广播不是循环给每一个点发送数据,而是给广播地址发送数据。
3. 广播地址是哪个?
gec@ubuntu:/mnt/hgfs/GZ1934/10 网络编程/02/code$ ifconfig
eth0 Link encap:Ethernet HWaddr 00:0c:29:5d:9c:76
inet addr:192.168.90.4 -> Ubuntu主机地址
Bcast:192.168.90.255 -> 广播地址
Mask:255.255.255.0 -> 子网掩码
4. 如何使得UDP协议客户端发送广播数据?
1)创建UDP套接字
sockfd = socket(UDP);
2)给UDP套接字设置一个广播属性
int on = 1;
setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));
例子:客户端!!!!
#include "head.h"
int main(int argc,char *argv[]) // ./Rose 192.168.90.4 50002
{
//1. 创建一个UDP协议的套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
//1.5 设置属性
int on = 1;
setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on));
//2. 直接写信
char buf[50];
struct sockaddr_in Roseaddr;
socklen_t len = sizeof(Roseaddr);
Roseaddr.sin_family = AF_INET;
Roseaddr.sin_port = htons(atoi(argv[2]));
inet_pton(AF_INET,argv[1],&Roseaddr.sin_addr);
while(1)
{
bzero(buf,sizeof(buf));
fgets(buf,50,stdin);
sendto(sockfd,buf,strlen(buf),0,(struct sockaddr *)&Roseaddr,len);
if(strncmp(buf,"quit",4) == 0)
{
break;
}
}
//3. 关闭套接字
close(sockfd);
}