前言
本文章主要是讲解Linux平台的网络通信,涉及的深度不是很深,但是覆盖的范围比较广,若需要学习更深的知识点,可以根据本文章涉及到的知识去深度挖去网络的资源学习下。
(一). 回顾系统编程进程的通信方式
无名管道,有名管道,信号,消息队列,共享内存,信号量 ---> 在同一个linux系统下 套接字通信 --> 跨主机 主机A 主机A Rose.c ---- Jack.c ---> 无名管道,有名管道,信号,消息队列,共享内存,信号量 主机A ---- 主机B Rose.c Jack.c ---> 套接字通信
(二). 网络编程大纲
1. 网络编程传输层协议 TCP / UDP 2. 关于网络概念知识 -- IP,端口号,字节序,socket 3. 网络通信4种IO模型 -- 阻塞,非阻塞,多路复用,信号驱动 4. 网络超时接收数据3种方式 -- alarm闹钟,多路复用,设置套接字的属性 5. 网络广播,组播 -- 基于UDP协议,组播组的IP分类,如何加入组?
(1)、 网络编程效果:
系统编程:自己Ubuntu --- 自己Ubuntu 网络编程: 条件: Ubuntu与开发板之间必须是在相同的网段中,网络是相通! 自己Ubuntu --- 自己Ubuntu 自己Ubuntu --- 自己开发板 自己Ubuntu --- 别人Ubuntu 自己Ubuntu --- 别人开发板 自己开发板 --- 别人Ubuntu 自己开发板 --- 别人开发板
(2)、 协议:在不同主机之间通信,双方都必须遵循一个原则。
Apanet协议: 不能互联不同类型的计算机与不同操作系统的两台主机 TCP/IP协议: 传输控制协议/因特网互联协议 TCP协议: 用于检测网络中差错。 IP协议: 负责在不同网络中进行通信。 TCP/IP协议 主机A ----------> 主机B 192.168.0.2 192.168.0.5 传输层: TCP协议 TCP协议 --> 一旦发生差错,就会马上重新传输,直到数据安全到达对方为止! 网络层: IP协议 IP协议 --> 分析IP地址
(三). 网络体系模型结构
1.所谓网络体系结构,指的是主机内部集成的结构与每层协议的集合,每台主机内部都会有这个模型。
2. 网络模型种类:
1)OSI模型(旧): 7层
现实例子: "hello" 老板发话 ---> 助理帮老板写信 ---> 前台帮助理寄信 --> 邮局职员送信 --> 邮局分地区职员 ---> 职员选择正确路线出发 --> 选择正确交通工具 OSI模型: -------------------用户层------------------- 应用层: 老板发话 表示层: 助理帮老板写信 会话层: 前台帮助理寄信 -------------------内核层------------------- 传输层: 邮局职员送信 网络层: 邮局分地区职员 广州/珠海 IP地址 -------------------驱动层------------------- 数据链路层: 职员选择正确路线出发 有线网卡/无线网卡 物理层: 选择正确交通工具 网口,网线 由于OSI模型处理数据效率非常低,这个模型已经被TCP/IP协议所取代
2)TCP/IP协议(新): 4层
现实例子: 老板自己想,自己写信,自己寄信 ---> 邮局职员送信 ---> 邮局分地区职员 --> 职员选择路线马上出发 TCP/IP协议模型: -------------------用户层------------------- 应用层: 老板自己想,自己写信,自己寄信 -------------------内核层------------------- 传输层: 邮局职员送信 网络层: 邮局分地区职员 广州/珠海 IP地址 -------------------驱动层------------------- 网络接口与物理层: 职员选择路线马上出发 3. 头数据 --> 每经过模型的一层,都会添加/删除一个头数据
例题: 现在主机A发送消息给主机B,简述工作原理。
(四). 网络编程重要概念socket、htons()、htonl()
1. socket ---> 插座,套接字, 插座类型繁多,就像协议一样,必须在通信设置好协议。
1) socket本身是一个函数接口,作用: 创建套接字
2) 无论TCP协议,还是UDP协议,都是使用socket函数去创建 int fd = socket(TCP协议); fd就是TCP套接字 --> 套接字文件 int fd = socket(UDP协议); fd就是UDP套接字 int fd = open("xxx"); --> 普通文件
3) 套接字是一种特殊的文件描述符 --> 都是可以用read/write
4) 在TCP/IP协议模型,socket处于应用层与传输层之间
2. IP地址
1)每一个主机内部系统只能有一个IP地址与之对应
2)IP地址 --> 32位
3)数据包中必须含有目标IP地址,源IP地址。
4)常常以点分式"192.168.0.102"
5)网络字节序是大端字节序
3. 端口号 --> 16位 0~65535
Jack.c ---> Rose.c IP地址: 192.168.0.2 192.168.0.10 --> 要求相同局域网 端口号: 50002 50002 端口号: 1) 系统占用端口号: 0~1023 2) 用户可用: 1024~65535
4. 字节序 h: host 本地字节序 to: 转换 n: net 网络字节序 l: 32位数据 s: 16位数据
htonl() --> 转IP地址 htons() --> 转端口号 原则: 不管是服务器还是客户端,统一在传输时把本地字节序转换为网络字节序
(五). TCP协议socket()、struct sockaddr_in、htons()、htonl()、socklen_t、bind()、listen()、accept()、recv()、connect()、send()
传输层协议:TCP协议(打电话)面向于有连接的通信方式
例题: 使用网络通信TCP协议,实现不同主机之间的通信
主机A---主机B Jack.c Rose.c
核心代码 Rose.c 服务器
创建未连接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
准备好服务器IP地址,端口号,协议 --> 通通塞到结构体中struct sockaddr_in
struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //网际协议 srvaddr.sin_port = htons(atoi(argv[1])); //端口号,atoi是把字符串转换成整型数 /* 之所以需要这些函数是因为计算机数据表示存在两种字节顺序:NBO与HBO 网络字节顺序NBO(Network Byte Order): 按从高到低的顺序存储,在网络上使用统一的网络字节顺序,可以避免兼容性问题。 主机字节顺序(HBO,Host Byte Order): 不同的机器HBO不相同,与CPU设计有关,数据的顺序是由cpu决定的,而与操作系统无关。 如 Intelx86结构下,short型数0x1234表示为34 12, int型数0x12345678表示为78 56 34 12如IBM power PC结构下,short型数0x1234表示为12 34, int型数0x12345678表示为12 34 56 78。 由于这个原因不同体系结构的机器之间无法通信,所以要转换成一种约定的数序,也就是网络字 节顺序,其实就是如同powerpc那样的顺序 。在PC开发中有ntohl和htonl函数可以用来进行网络字节和主机字节的转换。 */ srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* IP地址,由于服务器上可能有多个网卡,也就有多个ip地址,所以该ip地址的选项为INADDR_ANY,表示:在本服务器上无论是哪个ip地址接收到数据,只要是这个端口号,服务器都会处理。 */ INADDR_ANY --> 接收任何地址的数据信息 /* Address to accept any incoming messages. */ #define INADDR_ANY ((unsigned long int) 0x00000000)
把地址绑定到未连接套接字上
socklen_t就是struct sockaddr_in大小的数据类型 socklen_t len = sizeof(srvaddr); int ret = bind(fd,(struct sockaddr*)&srvaddr,len);
设置监听套接字
listen(fd,4); 是fd的本身从未连接套接字转换监听套接字 /* backlog参数就是控制我们的已连接队列里等待accept()取走的连接的最大数目的.注意一点,backlog与这个已排队连接的最大数目未必是完全相等的,不同的系统的实现可能不同.比如backlog=1,系统允许的实际一排队数目可能为2. */
坐等对方的连接
int connfd = accept(fd,(strutc sockaddr*)&cliaddr,&len);
畅聊
recv(connfd,buf,sizeof(buf),0);
断开连接
close(connfd); close(fd);
核心代码 Jack.c 客户端
创建未连接TCP套接字
int fd = socket(AF_INET,SOCK_STREAM,0);
发起连接
int ret = connect(fd,(struct sockaddr *)&srvaddr,len);
畅聊
send(fd,buf,strlen(buf),0);
运行步骤:
同桌Ubuntu 你的Ubuntu Rose.c Jack.c 192.168.0.10 192.168.0.20 50001 50001 同桌: ping 192.168.0.20 你: ping 192.168.0.10 同桌: ./Rose 50001 你: ./Jack 192.168.0.10 50001
例子1:
Jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Jack 服务器IP 端口号 ./Jack 192.168.0.2 50001 { //1. 创建未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); // 必须与服务器的类型一致 //2. 准备对方Rose的IP地址,端口号,协议 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //3. 发起连接 socklen_t len = sizeof(srvaddr); int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//连接成功后,fd自身就会变成已连接套接字 if(ret == -1) printf("connect error!\n"); else printf("connect ok!\n"); //4. 畅聊 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); send(fd,buf,strlen(buf),0); if(strncmp(buf,"quit",4) == 0) break; } //5. 挂断 close(fd); return 0; }
Rose.ccp
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #includeint main(int argc,char *argv[]) // ./Rose 50001 { //1. 创建一个未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); //2. 准备好服务器的结构体变量,再进行赋值 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //网际协议 srvaddr.sin_port = htons(atoi(argv[1])); //端口号 srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址 //3. 把服务器的IP地址,协议,端口号绑定到未连接套接字上 socklen_t len = sizeof(srvaddr); int ret = bind(fd,(struct sockaddr*)&srvaddr,len); if(ret == -1) printf("bind error!\n"); //4. 将未连接套接字转换为监听套接字 listen(fd,4); //5. 坐等电话 struct sockaddr_in cliaddr; //存放来电显示 int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待 if(connfd == -1) printf("accept error!\n"); else printf("connect ok!\n"); //6. 畅聊 char buf[50]; while(1) { bzero(buf,50); recv(connfd,buf,sizeof(buf),0); printf("from client:%s",buf); if(strncmp(buf,"quit",4) == 0) break; } //7. 挂断电话 close(connfd); close(fd); return 0; }
例子2:tcp_chat
Jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#include#include#include#include#includevoid *routine(void *arg) { int fd = *(int *)arg; char buf[50]; while(1) { bzero(buf,50); recv(fd,buf,sizeof(buf),0); printf("from Rose:%s",buf); if(strncmp(buf,"quit",4) == 0) { exit(0); } } } int main(int argc,char *argv[]) // ./Jack 服务器IP 端口号 ./Jack 192.168.0.2 50001 { //1. 创建未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); // 必须与服务器的类型一致 //2. 准备对方Rose的IP地址,端口号,协议 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //3. 发起连接 socklen_t len = sizeof(srvaddr); int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//连接成功后,fd自身就会变成已连接套接字 if(ret == -1) printf("connect error!\n"); else printf("connect ok!\n"); pthread_t tid; pthread_create(&tid,NULL,routine,(void *)&fd); //4. 畅聊 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); send(fd,buf,strlen(buf),0); if(strncmp(buf,"quit",4) == 0) break; } //5. 挂断 close(fd); return 0; }
Rose.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#include#include#include#includevoid *routine(void *arg) { int connfd = *(int *)arg; char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); send(connfd,buf,strlen(buf),0); if(strncmp(buf,"quit",4) == 0) { exit(0); } } } int main(int argc,char *argv[]) // ./Rose 50001 { //1. 创建一个未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); //2. 准备好服务器的结构体变量,再进行赋值 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //网际协议 srvaddr.sin_port = htons(atoi(argv[1])); //端口号 srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址 //3. 把服务器的IP地址,协议,端口号绑定到未连接套接字上 socklen_t len = sizeof(srvaddr); int ret = bind(fd,(struct sockaddr*)&srvaddr,len); if(ret == -1) printf("bind error!\n"); //4. 将未连接套接字转换为监听套接字 listen(fd,4); //5. 坐等电话 struct sockaddr_in cliaddr; //存放来电显示 int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待 if(connfd == -1) printf("accept error!\n"); else printf("connect ok!\n"); //5.5 创建线程,用于实现服务器写功能 pthread_t tid; pthread_create(&tid,NULL,routine,(void *)&connfd); //6. 畅聊 char buf[50]; while(1) { bzero(buf,50); recv(connfd,buf,sizeof(buf),0); printf("from client:%s",buf); if(strncmp(buf,"quit",4) == 0) break; } //7. 挂断电话 close(connfd); close(fd); return 0; }
(六).UDP协议recvfrom()、inet_pton()、sendto()
1. UDP协议 user data protrol 用户数据协议特点:
TCP: 面向连接 --> 一定双方连接上了才能进行通信!
UDP: 面向非连接 --> 不需要连接就可以进行数据的收发,提高效率。
UDP例子: 写信
2. UDP实现过程
例题: 客户端发送数据给服务器,使用UDP完成。
服务器:(收信) Rose.c
(1). 买一个信箱
int fd = socket(AF_INET,SOCK_DGRAM,0);
(2). 绑定一个地址到信箱
struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //协议 srvaddr.sin_port = htons(atoi(argv[1])); //端口号 srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); //IP地址 bind(fd,(struct sockaddr *)&srvaddr,len);
(3). 不断收信
recvfrom(fd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len);
(4). 销毁信箱
close(fd);
客户端:(写信)
(1). 买一个信箱
int fd = socket(AF_INET,SOCK_DGRAM,0);
(2). 准备服务器地址
struct sockaddr_in srvaddr; socklen_t len = sizeof(srvaddr); srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr);
(3). 不断往服务器地址写信
sendto(fd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len);
(4). 销毁信箱
close(fd);
例子:
Jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Jack 192.168.0.243 50002 { //1. 创建UDP套接字(没有地址的信箱) int fd = socket(AF_INET,SOCK_DGRAM,0); //2. 准备服务器的地址 struct sockaddr_in srvaddr; socklen_t len = sizeof(srvaddr); srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //AF_INET: 协议,与socket第一个参数一致 //argv[1]: 代表一个字符串,"192.168.0.243" //&srvaddr.sin_addr: 代表struct in_addr *类型,使用srvaddr变量访问sin_addr这个变量,再取地址就变成指针了! /* struct sockaddr_in { u_short sin_family; // 地址族 u_short sin_port; // 端口 struct in_addr sin_addr; // IPV4地址 char sin_zero[8]; }; */ //3. 不断写信 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); sendto(fd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len); if(strncmp(buf,"quit",4) == 0) break; } //4. 回收套接字资源 close(fd); return 0; }
Rose.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Rose 50001 { //1. 创建UDP套接字(没有地址的信箱) int fd = 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地址 //3. 绑定地址到套接字(把准备好的地址绑定到信箱上) bind(fd,(struct sockaddr *)&srvaddr,len); //4. 不断从UDP套接字中接收数据 struct sockaddr_in cliaddr; char buf[50]; while(1) { bzero(buf,50); //不断从fd这个信箱上读取cliaddr这个客户端给我发来的内容,然后存放在buf中 recvfrom(fd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len); printf("from Jack:%s",buf); if(strncmp(buf,"quit",4) == 0) break; } //5. 关闭套接字资源 close(fd); return 0; }
(七).四种IO模型
IO模型: 当信号到达时,程序如何处理这些数据?
方式: 阻塞,非阻塞,多路复用,信号驱动
四种IO模型特性:
1)阻塞IO
1.系统默认得到的文件描述符都是阻塞的 read(fd) recv(fd) recvfrom(fd); --> 这些函数本身不具有阻塞属性,而是这个文件描述符的本身具有阻塞的属性导致函数看起来好像阻塞一样! 2.由于socket套接字是特殊文件描述符,默认创建的套接字都是阻塞的!
2)非阻塞IO
1.给文件描述符添加非阻塞的属性 --> 缺点: 占用CPU资源较大,负荷大! 2.当非阻塞时,如果没有数据到达,那么读取数据就会失败,一定要不断询问套接字/文件描述符中是否有数据的到达!
3)多路复用
1.同时对多个IO口进行操作 2. 可以在规定的时间内检测数据是否到达 --> 超时知识
4)信号驱动
1.属于异步通信 --> 一定要给套接字/文件描述符设置信号触发模式属性 2. 在套接字/文件描述符有数据到达时,通过发送信号给用户,用户就知道有数据到达!
1、非阻塞IO,fcntl()
(1). 阻塞IO与非阻塞IO之间差异?
阻塞IO
建立套接字(默认是阻塞的) ---> 想读取套接字中数据 --> 判断缓冲区有没有数据? --> 没有 --> 进入无限等待的状态 --> 直到缓冲区数据为止 --> 读取数据 --> 没有 --> 进入无限等待的状态 --> 有 --> 读取数据 --> --> 没有 --> 进入无限等待的状态
非阻塞IO
建立套接字(默认是阻塞的) --> 添加非阻塞属性到套接字上 --> 想读取套接字中数据 --> 判断缓冲区有没有数据? --> 没有 --> 读取失败 ---> 接口马上返回,不会一直阻塞 --> 要是想再次读取,那么就要放在循环中 --> 有 --> 读取成功 ---> 接口也会返回
(2). 如何给套接字/文件描述符设置非阻塞属性? --- fcntl() --- man 2 fcntl
#include#includeint fcntl(int fd, int cmd, ... /* arg */ ); fd: 需要设置属性的文件描述符 cmd: 请求控制文件描述符的命令字 非阻塞的属性 arg: 这个参数要不要填,取决于cmd cmd: F_GETFL (void) Get the file access mode and the file status flags; arg is ignored. //获取文件的模式权限标志位,arg可以忽略了。 F_SETFL (long) Set the file status flags to the value specified by arg. File access mode (O_RDONLY, O_WRONLY, O_RDWR) and file creation flags (i.e., O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC) in arg are ignored. //以上提到的属性,不能通过fcntl()设置属性 On Linux this command can change only the O_APPEND, 文件追加属性 O_ASYNC, 信号触发模式 O_DIRECT, 不使用缓冲区写入 O_NOATIME, 不更新文件的修改时间 and O_NONBLOCK 非阻塞属性 flags. 注意: 在添加属性时,所有的属性使用 "|" 位或来计算 can be bitwise-or'd in flags. 返回值: 成功: F_GETFL Value of file status flags. F_SETFL 0 失败: -1
例子1:直接把文件描述符属性设置为非阻塞
fd = open("xxx"); fcntl(fd,F_SETFL,O_NONBLOCK);
例子2:创建一个套接字,在套接字原来的属性的基础上添加非阻塞属性。
int fd = socket(xxx); int state = fcntl(fd,F_GETFL); //获取文件原来的属性 state |= O_NONBLOCK; //在原来的基础上添加非阻塞的属性 fcntl(fd,F_SETFL,state); //设置state属性到套接字上
例子3:给TCP通信设置非阻塞属性
Jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Jack 服务器IP 端口号 ./Jack 192.168.0.2 50001 { //1. 创建未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); // 必须与服务器的类型一致 //2. 准备对方Rose的IP地址,端口号,协议 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //3. 发起连接 socklen_t len = sizeof(srvaddr); int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//连接成功后,fd自身就会变成已连接套接字 if(ret == -1) printf("connect error!\n"); else printf("connect ok!\n"); //4. 畅聊 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); send(fd,buf,strlen(buf),0); if(strncmp(buf,"quit",4) == 0) break; } //5. 挂断 close(fd); return 0; }
Rose.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#include#includeint main(int argc,char *argv[]) // ./Rose 50001 { //1. 创建一个未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); //2. 准备好服务器的结构体变量,再进行赋值 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //网际协议 srvaddr.sin_port = htons(atoi(argv[1])); //端口号 srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址 //3. 把服务器的IP地址,协议,端口号绑定到未连接套接字上 socklen_t len = sizeof(srvaddr); int ret = bind(fd,(struct sockaddr*)&srvaddr,len); if(ret == -1) printf("bind error!\n"); //4. 将未连接套接字转换为监听套接字 listen(fd,4); //5. 坐等电话 struct sockaddr_in cliaddr; //存放来电显示 int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待 if(connfd == -1) printf("accept error!\n"); else printf("connect ok!\n"); //5.5. 设置非阻塞属性到connfd int state = fcntl(connfd,F_GETFL); state |= O_NONBLOCK; fcntl(connfd,F_SETFL,state); //6. 畅聊 char buf[50]; while(1) { bzero(buf,50); recv(connfd,buf,sizeof(buf),0); printf("from client:%s\n",buf); //usleep(100000); if(strncmp(buf,"quit",4) == 0) break; } //7. 挂断电话 close(connfd); close(fd); return 0; }
例子3:写一个服务器,实现全部连接到该服务器的用户存放在链表中,可实现群发,私聊,客户端退出等功能。
以“:”形式区别群发内容与私聊内容
例如:hello就是群发
103:hello就是给端口为103的用户发送hello的消息
提示: strstr() 可以判断某个字符串内是否有某个字符 :
使用方法 char *strstr(char *str1, char *str2); 意义为 判断str2是否为str1的子串,若是则返回str2在str1中首次出现的指针位置,若不是返回NULL;
atoi()只会判断数字,即遇到非数字就会停止转化。
itoa():将整型值转换为字符串。
例子: int a = atoi(“103:hello”) --> 只会把103转为int型,不会理会后面的字符串:hello
Jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#include#include#include#include#includevoid *routine(void *arg) { int fd = *(int *)arg; char buf[50]; while(1) { bzero(buf,50); recv(fd,buf,sizeof(buf),0); printf("from Rose:%s",buf); if(strncmp(buf,"quit",4) == 0) { exit(0); } } } int main(int argc,char *argv[]) // ./Jack 服务器IP 端口号 ./Jack 192.168.0.2 50001 { //1. 创建未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); // 必须与服务器的类型一致 //2. 准备对方Rose的IP地址,端口号,协议 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //3. 发起连接 socklen_t len = sizeof(srvaddr); int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//连接成功后,fd自身就会变成已连接套接字 if(ret == -1) printf("connect error!\n"); else printf("connect ok!\n"); pthread_t tid; pthread_create(&tid,NULL,routine,(void *)&fd); //4. 畅聊 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); send(fd,buf,strlen(buf),0); if(strncmp(buf,"quit",4) == 0) break; } //5. 挂断 close(fd); return 0; }
server.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#include#include#include "kernel_list.h" #include//初始化锁变量 pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; struct client *head = NULL; //设计内核链表节点 struct client{ int connfd; //数据域 struct list_head list; //指针域 }; struct client *init_head(struct client *head) { head = (struct client *)malloc(sizeof(struct client)); INIT_LIST_HEAD(&(head->list)); return head; } int msg_broadcast(char *msg,struct client *sender) //hello { struct client *p = NULL; pthread_mutex_lock(&m); //p: 遍历链表的指针 //&(head->list): 头节点指针域的地址 list_for_each_entry(p,&(head->list),list) { //除了发送者自己,其他人的都要收到该消息 if(p->connfd == sender->connfd) { continue; } send(p->connfd,msg,strlen(msg),0); } pthread_mutex_unlock(&m); return 0; } int msg_send(int receive_connfd,char *msg) { struct client *p = NULL; pthread_mutex_lock(&m); list_for_each_entry(p,&(head->list),list) { //找到那个私聊的人了 if(p->connfd == receive_connfd) { send(p->connfd,msg,strlen(msg),0); pthread_mutex_unlock(&m);//找到了解锁 return 0;//找到了就不用继续找了,提前退出! } } pthread_mutex_unlock(&m);//找不到解锁 return -1; } void *routine(void *arg) { struct client* peer = (struct client *)arg; char msg[200]; //各个线程只需要负责不断读取对应的客户端的数据 while(1) { bzero(msg,200); read(peer->connfd,msg,sizeof(msg)); printf("msg = %s",msg); //1. 客户端退出 if(strncmp(msg,"quit",4) == 0) { close(peer->connfd); list_del(&(peer->list)); free(peer); break; } //2. 群发 没有: char *tmp = NULL; tmp = strstr(msg,":"); if(tmp == NULL) { msg_broadcast(msg,peer); } //3. 私聊 有 5:hello else{ int receive_connfd = atoi(msg);//5 if(msg_send(receive_connfd,tmp+1) == -1) { printf("NOT FOUNT client!\n"); } } } } int main(int argc,char *argv[]) { //1. 初始化链表头 head = init_head(head); //2. 创建TCP套接字 int sockfd = socket(AF_INET,SOCK_STREAM,0); //3. 准备好服务器的结构体变量,再进行赋值 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //网际协议 srvaddr.sin_port = htons(atoi(argv[1])); //端口号 srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址 //4. 把服务器的IP地址,协议,端口号绑定到未连接套接字上 socklen_t len = sizeof(srvaddr); int ret = bind(sockfd,(struct sockaddr*)&srvaddr,len); if(ret == -1) printf("bind error!\n"); //5. 将未连接套接字转换为监听套接字 listen(sockfd,4); //6. 不断等待客户端连接到服务器中,只要连接上,就尾插到链表的末尾! struct sockaddr_in cliaddr; int connfd; while(1) { bzero(&cliaddr,len); connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&len); printf("connfd = %d\n",connfd); printf("new connection:%s\n",(char *)inet_ntoa(cliaddr.sin_addr)); struct client *new = (struct client *)malloc(sizeof(struct client)); if(new != NULL) { //如果新建的节点申请空间成功,那么就进行赋值 new->connfd = connfd; } //尾插这个节点到链表的末尾 //只要修改链表的长度,以及访问该链表,都要上锁 pthread_mutex_lock(&m); list_add_tail(&(new->list),&(head->list)); pthread_mutex_unlock(&m); //只要添加了新的用户,就为这个用户分配一个线程,用于管理这个用户将来想做的事情 pthread_t tid; pthread_create(&tid,NULL,routine,(void *)new); } }
2、多路复用select()、FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()
(1).同时监听多个IO口? ---- fd1 fd2 fd3 sockfd1 sockfd2 --> 集合中 --> 监听集合就知道是谁有数据变化
阻塞IO? --> 监听单个IO口,不能同时监听多个。 非阻塞IO? --> 监听多个IO口,但是占用CPU资源非常大。 --> 监听多个IO口,又想不占用太多CPU资源 --> 多路复用。
(2).什么是多路复用? 工作原理?
首先用户预先将需要进行监听的所有的文件描述符加入集合中,然后在规定的时间/无限时间内无限等待集合。如果在规定的时间集合中文件描述符没有数据变化,就会进入下一次规定时间内的等待。一旦集合中的文件描述符有数据变化,则其他没有数据变化的文件描述符会被剔除到集合之外,并再次进入下一次的等待状态。
(3).多路复用函数接口 --- select() --- man 2 select
#include#include#include#includeint select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); nfds: 集合中的所有文件描述符最大值+1 readfds: 所有关注"是否存在待读取数据"的文件描述符集合 套接字sockfd 键盘:STDIN_FILENO 99% writefds: 所有关注"是否有可传输非阻塞" 的文件描述符集合 -->0.5% NULL exceptfds: 所有关注"是否发生异常"的文件描述符集合 -->0.5% NULL timeout: 设置最大等待时间 ---> 超时一次,重新设置该值,再传递给select函数 ---> 如果该参数填NULL,则是无限等待 struct timeval { long tv_sec; 秒 long tv_usec; 微秒 1秒 = 1000000微秒 ---> select函数可以精确到小数点后6位 }; 返回值: 成功: 有数据达到 --> 就绪的文件描述符的总数 在规定的时间没有数据到达 --> 0 失败: -1
(4)、处理集合的函数
1)删除集合set中某个文件描述符fd
void FD_CLR(int fd, fd_set *set);
2)判断某个文件描述符fd是否在集合set中
int FD_ISSET(int fd, fd_set *set); --> this is useful after select() returns. 返回值: fd在集合中: 1 fd不在集合中: 0
3)把文件描述符fd加入到集合set中
void FD_SET(int fd, fd_set *set);
4)清空集合set
void FD_ZERO(fd_set *set);
例题:
实现客户端与服务器进行收发,5秒内等待数据的到达! 如果5秒内没有数据到达,则打印timeout!
客户端 服务器 收 fd connfd --> 可以知道客户端有没有数据发送过来 发 STDIN_FILENO STDIN_FILENO --> 监听自己的键盘有没有数据的输入 服务器/客户端模型: 1. 处理TCP流程 2. 得到connfd/fd 3. 把connfd/fd与STDIN_FILENO加入读集合readfdset中 4. 使用select函数监听该集合 5. 判断文件描述符是否在集合中 if(FD_ISSET(connfd/fd,&set) == 1) { read(connfd/fd,buf); } if(FD_ISSET(STDIN_FILENO,&set) == 1) { fgets(buf,50,stdin); }
jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Jack 服务器IP 端口号 ./Jack 192.168.0.2 50001 { //1. 创建未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); // 必须与服务器的类型一致 //2. 准备对方Rose的IP地址,端口号,协议 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //3. 发起连接 socklen_t len = sizeof(srvaddr); int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//连接成功后,fd自身就会变成已连接套接字 if(ret == -1) printf("connect error!\n"); else printf("connect ok!\n"); //4. 畅聊 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); send(fd,buf,strlen(buf),0); if(strncmp(buf,"quit",4) == 0) break; } //5. 挂断 close(fd); return 0; }
server.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#include#include#include#include#include#include#include#includevoid *routine(void*arg) { int i=0; while(1) { printf("%d\n",i++); sleep(1); } } int main(int argc,char *argv[]) { //0. 创建线程,用于计算时间流逝 pthread_t tid; pthread_create(&tid,NULL,routine,NULL); //1. 创建套接字 int fd = socket(AF_INET,SOCK_STREAM,0); //2. 准备好服务器的结构体变量,再进行赋值 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //网际协议 srvaddr.sin_port = htons(atoi(argv[1])); //端口号 srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址 //3. 把服务器的IP地址,协议,端口号绑定到未连接套接字上 socklen_t len = sizeof(srvaddr); int ret = bind(fd,(struct sockaddr*)&srvaddr,len); if(ret == -1) printf("bind error!\n"); //4. 将未连接套接字转换为监听套接字 listen(fd,4); //5. 坐等电话 struct sockaddr_in cliaddr; //存放来电显示 int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待 if(connfd == -1) printf("accept error!\n"); else printf("connect ok!\n"); //6. 把需要监听的connfd加入集合中 fd_set rset; struct timeval v; char buf[50]; while(1) { //不管有没有超时,每次都把套接字加入集合中 FD_ZERO(&rset); FD_SET(connfd,&rset); //重新设置超时时间 v.tv_sec = 5; v.tv_usec = 0; ret = select(connfd+1,&rset,NULL,NULL,&v); //在5秒钟内没有数据达到,就打印timeout if(ret == 0) { printf("timeout!\n"); } //select函数执行失败 if(ret == -1) { printf("select error!\n"); } //在5秒内有数据达到,就打印数据 if(FD_ISSET(connfd,&rset) == 1) { bzero(buf,50); recv(connfd,buf,sizeof(buf),0); printf("buf:%s",buf); if(strncmp(buf,"quit",4) == 0) break; } } }
3.信号驱动signal()、fcntl()、
(1). 信号驱动工作原理是什么?
就是使用信号机制,首先安装信号SIGIO处理函数,通过监听文件描述符是否产生了SIGIO信号,当数据到达时,就等于产生该信号,用户读取该数据。
(2). 特点
1)信号驱动一般作用于UDP协议,很少作用于TCP协议,因为TCP协议中有多次IO口变化,难以捕捉信号。
2)由于有数据变化时,会产生一个信号,所以我们提前捕捉 -- signal(捕捉的信号,处理函数);
3)必须要给套接字/文件描述符设置添加一个信号触发模式
(3). 在一个套接字上使用信号驱动,下面的三步是必须设置:
1)捕捉信号,设置信号的处理函数
signal(SIGIO,fun); --> fun()进行IO操作
2)设置套接字的拥有者(系统中有可能有很多套接字,必须提前告知是本进程的套接字)
F_SETOWN (long) Set the process ID or process group ID that will receive SIGIO and SIGURG signals for events on file descriptor fd to the ID given in arg. A process ID is specified as a positive value; fcntl(fd,F_SETOWN,getpid());
3)给套接字添加信号触发模式
int state; state = fcntl(fd,F_GETFL); state |= O_ASYNC; fcntl(fd,F_SETFL,state);
例题:
使用IO模型中信号驱动方式写一个UDP服务器,实现不断读取客户端消息。
思路: 1. 建立UDP套接字 2. 捕捉,设置拥有者,添加信号触发模式 3. 一旦有数据到达,那么就在信号处理函数中不断打印客户端消息
jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Jack 192.168.0.243 50002 { //1. 创建UDP套接字(没有地址的信箱) int fd = socket(AF_INET,SOCK_DGRAM,0); //2. 准备服务器的地址 struct sockaddr_in srvaddr; socklen_t len = sizeof(srvaddr); srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //AF_INET: 协议,与socket第一个参数一致 //argv[1]: 代表一个字符串,"192.168.0.243" //&srvaddr.sin_addr: 代表struct in_addr *类型,使用srvaddr变量访问sin_addr这个变量,再取地址就变成指针了! /* struct sockaddr_in { u_short sin_family; // 地址族 u_short sin_port; // 端口 struct in_addr sin_addr; // IPV4地址 char sin_zero[8]; }; */ //3. 不断写信 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); sendto(fd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len); if(strncmp(buf,"quit",4) == 0) break; } //4. 回收套接字资源 close(fd); return 0; }
server.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#include#include#include#include#include#include#includeint sockfd; void fun(int sig) { printf("catch sig:%d\n",sig); char buf[50]; struct sockaddr_in cliaddr; socklen_t len = sizeof(cliaddr); bzero(buf,50); bzero(&cliaddr,len); recvfrom(sockfd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len); printf("from client:%s",buf); return; } int main(int argc,char *argv[]) // ./server 50001 { //1. 创建UDP套接字 sockfd = socket(AF_INET,SOCK_DGRAM,0); //2. 绑定IP地址到套接字上 struct sockaddr_in srvaddr; bzero(&srvaddr,sizeof(srvaddr)); srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[1])); srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(sockfd,(struct sockaddr *)&srvaddr,sizeof(srvaddr)); //3. 捕捉SIGIO信号,设置信号处理函数 signal(SIGIO,fun); //4. 设置套接字的拥有者 fcntl(sockfd,F_SETOWN,getpid()); //5. 给套接字添加信号触发模式 int state; state = fcntl(sockfd,F_GETFL); state |= O_ASYNC; fcntl(sockfd,F_SETFL,state); //6. 挂起进程,不退出 while(1) pause(); return 0; }
(八).设置属性函数setsockopt()
setsockopt设置属性函数 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:选项名字 optval:值,使能为1,不使能为0 int struct timeval optlen:值类型大小 sizeof(int) sizeof(struct timeval) optname: ===========================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
(九).网络超时接收select、alarm、setsockopt
一般地,默认是阻塞等待读取数据。有些场合不需要使用一直阻塞。因为一直阻塞可能没有结果。这时候可以使用超时接收,在规定的时间内接收数据,超过规定的时间,就不会再阻塞。
设置超时接收数据方式: 1. 使用多路复用select函数设置超时时间。 2. 设置闹钟,当时间到达时,就会产生一个信号进行提醒,即超时。 3. 设置套接字本身的属性为超时接收。
1、使用多路复用select函数设置超时时间。
例题:写一个服务器进行接收数据,使用select函数监听客户端状态,如果在5秒内没有数据到达,则超时。
select只需要监听 --> connfd --> 如果select返回值为0,则超时。
核心代码:
while(1) { //不管有没有超时,每次都把套接字加入集合中 FD_ZERO(&rset); FD_SET(connfd,&rset); //重新设置超时时间 v.tv_sec = 5; v.tv_usec = 0; ret = select(connfd+1,&rset,NULL,NULL,&v); //只需要判断套接字是否在集合中即可! }
jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Jack 服务器IP 端口号 ./Jack 192.168.0.2 50001 { //1. 创建未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); // 必须与服务器的类型一致 //2. 准备对方Rose的IP地址,端口号,协议 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //3. 发起连接 socklen_t len = sizeof(srvaddr); int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//连接成功后,fd自身就会变成已连接套接字 if(ret == -1) printf("connect error!\n"); else printf("connect ok!\n"); //4. 畅聊 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); send(fd,buf,strlen(buf),0); if(strncmp(buf,"quit",4) == 0) break; } //5. 挂断 close(fd); return 0; }
server.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#include#include#include#include#include#include#include#includevoid *routine(void*arg) { int i=0; while(1) { printf("%d\n",i++); sleep(1); } } int main(int argc,char *argv[]) { //0. 创建线程,用于计算时间流逝 pthread_t tid; pthread_create(&tid,NULL,routine,NULL); //1. 创建套接字 int fd = socket(AF_INET,SOCK_STREAM,0); //2. 准备好服务器的结构体变量,再进行赋值 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //网际协议 srvaddr.sin_port = htons(atoi(argv[1])); //端口号 srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址 //3. 把服务器的IP地址,协议,端口号绑定到未连接套接字上 socklen_t len = sizeof(srvaddr); int ret = bind(fd,(struct sockaddr*)&srvaddr,len); if(ret == -1) printf("bind error!\n"); //4. 将未连接套接字转换为监听套接字 listen(fd,4); //5. 坐等电话 struct sockaddr_in cliaddr; //存放来电显示 int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待 if(connfd == -1) printf("accept error!\n"); else printf("connect ok!\n"); //6. 把需要监听的connfd加入集合中 fd_set rset; struct timeval v; char buf[50]; while(1) { //不管有没有超时,每次都把套接字加入集合中 FD_ZERO(&rset); FD_SET(connfd,&rset); //重新设置超时时间 v.tv_sec = 5; v.tv_usec = 0; ret = select(connfd+1,&rset,NULL,NULL,&v); //在5秒钟内没有数据达到,就打印timeout if(ret == 0) { printf("timeout!\n"); } //select函数执行失败 if(ret == -1) { printf("select error!\n"); } //在5秒内有数据达到,就打印数据 if(FD_ISSET(connfd,&rset) == 1) { bzero(buf,50); recv(connfd,buf,sizeof(buf),0); printf("buf:%s",buf); if(strncmp(buf,"quit",4) == 0) break; } } }
2、设置闹钟,当时间到达时,就会产生一个信号进行提醒,即超时。
闹钟这种方式类似信号驱动,信号驱动收到信号时,证明有数据过来。闹钟使用alarm函数来提前设置一个时间,当时间到达时,就会自动产生一个信号,证明超时。
例子: 设置一个闹钟,时间为5秒 --> 当时间到达时,就会自动产生一个SIGALRM信号。 14) SIGALRM
如何设定一个闹钟 --- alarm --- man 2 alarm
#includeunsigned int alarm(unsigned int seconds); seconds: 闹钟设置的时间 unsigned int --> 参数不能填负数! //在seconds这么多秒之后就会产生一个SIGALRM信号给正在运行的进程 alarm() arranges for a SIGALRM signal to be delivered to the calling process in seconds seconds. //如果秒数为0,不会预设定闹钟 If seconds is zero, no new alarm() is scheduled. //任何的事件都可以使用alarm()取消 --> 重新设定时间,闹钟到点就不会响应。 In any event any previously set alarm() is canceled. alarm(5); --> 如果顺利倒数5秒,则会产生一个信号 SIGALRM 如果被重新设定时间,重新倒数! 返回值: 返回剩余的时间,如果倒数完了,返回0
例子:
while(1) { alarm(5); --> 倒数完,会产生一个信号SIGALRM ....; ....; --> 如果在这个地方阻塞了,就不会再去执行alarm(5),没有重新预设定时间。 ....; }
例题: 设置一个闹钟,让客户端必须在5秒之内发送数据给服务器,如果服务器在5秒内收到数据,则重新倒数5秒。
如果在5秒内没有收到数据,打印timeout。直到收到数据为止再去重新设置闹钟来倒数5秒。
jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Jack 服务器IP 端口号 ./Jack 192.168.0.2 50001 { //1. 创建未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); // 必须与服务器的类型一致 //2. 准备对方Rose的IP地址,端口号,协议 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //3. 发起连接 socklen_t len = sizeof(srvaddr); int ret = connect(fd,(struct sockaddr *)&srvaddr,len);//连接成功后,fd自身就会变成已连接套接字 if(ret == -1) printf("connect error!\n"); else printf("connect ok!\n"); //4. 畅聊 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); send(fd,buf,strlen(buf),0); if(strncmp(buf,"quit",4) == 0) break; } //5. 挂断 close(fd); return 0; }
rose.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#include#includevoid *routine(void *arg) { int i = 0; while(1) { printf("%d\n",i++); sleep(1); } } void fun(int sig) { printf("catch sig = %d\n",sig); printf("timeout!\n"); } int main(int argc,char *argv[]) // ./Rose 50001 { //0. 创建线程 pthread_t tid; pthread_create(&tid,NULL,routine,NULL); signal(SIGALRM,fun); //1. 创建一个未连接TCP套接字 int fd = socket(AF_INET,SOCK_STREAM,0); //2. 准备好服务器的结构体变量,再进行赋值 struct sockaddr_in srvaddr; srvaddr.sin_family = AF_INET; //网际协议 srvaddr.sin_port = htons(atoi(argv[1])); //端口号 srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); // IP地址 //3. 把服务器的IP地址,协议,端口号绑定到未连接套接字上 socklen_t len = sizeof(srvaddr); int ret = bind(fd,(struct sockaddr*)&srvaddr,len); if(ret == -1) printf("bind error!\n"); //4. 将未连接套接字转换为监听套接字 listen(fd,4); //5. 坐等电话 struct sockaddr_in cliaddr; //存放来电显示 int connfd = accept(fd,(struct sockaddr*)&cliaddr,&len); //阻塞等待 if(connfd == -1) printf("accept error!\n"); else printf("connect ok!\n"); //6. 畅聊 char buf[50]; while(1) { alarm(5); //如果5秒到了,就会产生一个信号SIGALRM,但是阻塞在recv bzero(buf,50); recv(connfd,buf,sizeof(buf),0); printf("from client:%s",buf); if(strncmp(buf,"quit",4) == 0) break; } //7. 挂断电话 close(connfd); close(fd); return 0; }
4、设置套接字本身的属性为超时接收。
在Linux中,默认创建的套接字都是阻塞属性,我们需要设置一个超时属性给套接字,这样读取套接字中数据时,在规定的时间之内会阻塞,在规定的时间之外,读取失败。
1. 例子: int connfd = accept(fd); read(connfd); --> 读取会一直阻塞 int connfd = accept(fd); 设置一个超时的时间给connfd read(connfd); --> 有数据 --> 读取出来 --> 在规定的时间没有数据 --> read函数就会马上返回失败,不会一直等待!
如何设置属性给套接字? --- setsockopt --- man 2 setsockopt
#include/*See NOTES*/ #includeint setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); sockfd:需要设置属性的套接字 level:优先级 SOL_SOCKET:套接字 IPPROTO_IP:IP优先级 IPPRO_TCP:TCP优先级 optname:选项名字 optval:值,使能为1,不使能为0 int struct timeval optlen:值类型大小 sizeof(int) sizeof(struct timeval) //返回值: 成功: 0 失败: -1
例子: 添加一个接受超时属性给套接字connfd
struct timeval v; v.tv_sec = 5; v.tv_usec = 0; setsockopt(connfd,SOL_SOCKET,SO_RCVTIMEO,&v,sizeof(v));
例题:使用套接字设置属性函数设置超时属性,如果服务器在6秒内没有数据到达,则打印timeout!
(十).广播、组播setsockopt
1. 广播
之前介绍所有例子: "点对点" --> 在socket称之为单播
如果给局域网中所有的主机发送数据: "点对多" --> 广播
(1). 广播特点:
1)不是循环地给每个点发送数据,而是在一个局域网中,给广播的地址(xxx.xxx.xxx.255)发送消息 2)只需要给广播地址发送消息,整个网段的主机都会收到消息 192.168.1.100 192.168.1.243 192.168.1.255 “hello” 3)只有UDP协议才能使用广播
(2). 广播地址:
gec@ubuntu:/mnt/hgfs/fx9/02 网络编程/03/code/timeout/setsockopt$ ifconfig eth0 Link encap:Ethernet HWaddr 00:0c:29:f5:92:f6 inet addr:192.168.0.243 ---> 当前主机的IP地址 Bcast:192.168.0.255 ---> 广播地址 Mask:255.255.255.0 ---> 子网掩码 如果给192.168.0.255发送数据,那么整个“192.168.0.xx”网段主机都会收到消息 如果给255.255.255.255发送数据,无论你是什么网段的主机,都会收到消息
(3). 如何使得客户端发送广播数据?
在Linux中创建套接字默认是没有广播的属性,所以手动添加广播属性给套接字
1)建立UDP套接字 int sockfd = socket(UDP协议); --> sockfd是没有广播属性 2)设置广播的属性给套接字 setsockopt(sockfd,广播属性); 3)往广播的地址上发送数据 inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); // ./Jack 192.168.0.255 50001
例题:写一个客户端,实现广播地发送消息
Ubuntu: 192.168.0.243 ./server 50001 同桌: 192.168.0.244 ./server 50001 写一个广播客户端client: 执行: ./client 192.168.0.243 50001 单播 ./client 192.168.0.255 50001 广播 ./client 255.255.255.255 50001 广播
2. 组播
组播算是单播与广播之间的折中,在一个局域网中,把某些主机加入组,设置一个IP地址给组。将来我们只需要往组的地址上发送数据,那么加入该组的所有主机都会收到数据。
(1). 特点:
1)在组播之前必须为组设置一个D类地址作为该组的一个IP地址 224.0.0.10 2)只有UDP协议才能实现组播
(2). IP地址分类: 192.168.0.100(网络字节+主机字节)
网络字节 主机字节 范围 A类地址: 1字节 3字节 1.0.0.1 ~ 126.255.255.255 B类地址: 2字节 2字节 128.0.0.1 ~ 191.255.255.255 C类地址: 3字节 1字节 192.0.0.1 ~ 223.255.255.255 D类地址: 不区分网络字节与主机字节 224.0.0.1 ~ 239.255.255.255
(3). 服务器怎么接受组播消息? --> 需要添加加入组播属性到套接字上
加入组播属性: IP_ADD_MEMBERSHIP 加入组播 struct ip_mreq 该结构体是被定义在Ubuntu: /usr/include/linux/in.h struct ip_mreq { struct in_addr imr_multiaddr; //组播的组的IP地址 224.0.0.10 struct in_addr imr_interface; //需要加入到组里面IP地址 192.168.0.243 -> 就是这个IP地址进组 }; struct in_addr { in_addr_t s_addr; // 无符号32位网络地址 };
服务器框架:
1)建立UDP套接字 int sockfd = socket(UDP协议); 2)定义struct ip_mreq变量 struct ip_mreq v; inet_pton(AF_INET,"224.0.0.10",&v.imr_multiaddr); inet_pton(AF_INET,"192.168.0.243",&v.imr_interface); 3)加入组播属性到套接字上 setsockopt(sockfd,.........,&v,sizeof(v)); 4)坐等组播消息
客户端框架:
1)建立UDP套接字 int sockfd = socket(UDP协议); 2)设置广播的属性给套接字 setsockopt(sockfd,广播属性); 3)发送数据给服务器 ./Jack 192.168.0.243 50001 单播 ./Jack 224.0.0.10 50001 组播 ./Jack 192.168.0.255 50001 广播 ./Jack 255.255.255.255 50001 广播
例子: 服务器1 --> 224.0.0.10
服务器2 --> 224.0.0.10
服务器3不加入组
./Jack 224.0.0.10 50001 --> 只有服务器1与服务器2才能收到数据
jack.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Jack 192.168.0.255 50002 { //1. 创建UDP套接字(没有地址的信箱) int fd = socket(AF_INET,SOCK_DGRAM,0); //1.5 设置套接字的广播属性 int on = 1; setsockopt(fd,SOL_SOCKET,SO_BROADCAST,&on,sizeof(on)); //2. 准备服务器的地址 struct sockaddr_in srvaddr; socklen_t len = sizeof(srvaddr); srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(atoi(argv[2])); inet_pton(AF_INET,argv[1],&srvaddr.sin_addr); //AF_INET: 协议,与socket第一个参数一致 //argv[1]: 代表一个字符串,"192.168.0.243" //&srvaddr.sin_addr: 代表struct in_addr *类型,使用srvaddr变量访问sin_addr这个变量,再取地址就变成指针了! /* struct sockaddr_in { u_short sin_family; // 地址族 u_short sin_port; // 端口 struct in_addr sin_addr; // IPV4地址 char sin_zero[8]; }; */ //3. 不断写信 char buf[50]; while(1) { bzero(buf,50); fgets(buf,50,stdin); sendto(fd,buf,strlen(buf),0,(struct sockaddr *)&srvaddr,len); if(strncmp(buf,"quit",4) == 0) break; } //4. 回收套接字资源 close(fd); return 0; }
rose.c
#include/* See NOTES */ #include#include#include//18.04 16.04 删除这个头文件 #include#includeint main(int argc,char *argv[]) // ./Rose 50001 { //1. 创建UDP套接字(没有地址的信箱) int fd = 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地址 //3. 绑定地址到套接字(把准备好的地址绑定到信箱上) bind(fd,(struct sockaddr *)&srvaddr,len); //4. 不断从UDP套接字中接收数据 struct sockaddr_in cliaddr; char buf[50]; while(1) { bzero(buf,50); //不断从fd这个信箱上读取cliaddr这个客户端给我发来的内容,然后存放在buf中 recvfrom(fd,buf,sizeof(buf),0,(struct sockaddr *)&cliaddr,&len); printf("from %s : %s",(char *)inet_ntoa(cliaddr.sin_addr),buf); if(strncmp(buf,"quit",4) == 0) break; } //5. 关闭套接字资源 close(fd); return 0; }