B站视频:千峰物联网学科linux网络编程
网址:https://www.bilibili.com/video/BV1RJ411B761?p=1
1、建立链接->使用链接->释放链接
2、物理通路被通信双方独占
计算机数据是突发式出现在数据链路上的,而电路交换网的建立链接、使用链接、释放链接的三个过程使得传输效率太低,故电路交换不适合传输计算机数据。
1957年10月4日,苏联发射了世界上第一颗人造地球卫星——Sputnik
针对Sputnik所带来的威胁,美国国会于1958年1月7日拨款成立ARPA(the Advanced Research Projects Agency 美国高级研究计划署)
对计算机网络的要求
1、不是为了打电话
2、结构简单,可靠的传输数据
3、能够连接不同种类的计算机
4、所有网络节点同等重要
5、必须有冗余的路由(不同路径的选择)
通过标有地址的分组进行路由选择传送数据,使通信通道仅在传送期间被占用的一种交换方式——比如下载英雄联盟,大文件肯定要分开走不同的路径下载
分组的组成:
每个分组都由首部和数据段组成;为什么?——携带信息,比如第几个数据、目的地、发送地地址等等
交换方式—存储转发
节点收到分组,先暂时存储下来,再检查其首部,按
照首部中的目的地址,找到合适的节点转发出去(路由器做的工作,路由表)
特点:
1、以分组作为传输单位(下载英雄联盟数据分开传)
2、独立的选择转发路由(不同的数据包走的路由线路是不同的)
3、逐段占用,动态分配传输带宽
想一想:节点收到的分组有序吗?——不是的,数据包到达的时间不一定是有序的
从单个ARPANET(阿帕网)向因特网发展的过程
1983年TCP/IP协议成为APRANET的标准协议
1.1.6.1三级结构的因特网
(NSFNET国家科学基金网)
围绕六台大型计算机中心建设起来的计算机网络
主干网、地区网、校园网
blog.csdnimg.cn/02d4e68f966547dba982064d67e84313.png)
1.1.6.2多级结构因特网
NSFNET逐步被商用因特网主干网替代
为了使各种不同的计算机之间可以互联,ARPANet指定了一套计算机通信协议,即TCP/IP协议(族)
为了减少协议设计的复杂性,大多数网络模型均采用分层的方式来组织
每一层利用下一层提供的服务来为上一层提供服务
本层服务的实现细节对上层屏蔽
每层完成自己的任务,最终通过不同层次的处理完成数据的收发
特指为实现在一个相互连接的网络系统上从源地址到目的地传输数据包(互联网数据包)所提供必要功能的协议
特点:
不可靠:它不能保证IP数据包能成功地到达它的目的地,仅提供尽力而为的传输服务
无连接:IP并不维护任何关于后续数据包的状态信息。每个数据包的处理是相互独立的。IP数据包可以不按发送顺序接收
IP数据包中含有发送它主机的IP地址(源地址)和接收它主机的IP地址(目的地址)
TCP是一种面向连接的,可靠的传输层通信协议
功能:
提供不同主机上的进程间通信
特点
1、建立链接->使用链接->释放链接(虚电路)
2、TCP数据包中包含序号和确认序号
3、对包进行排序并检错,而损坏的包可以被重传
服务对象
需要高度可靠性且面向连接的服务
如HTTP、FTP、SMTP等
UDP是一种面向无连接的传输层通信协议
功能:
提供不同主机上的进程间通信
特点
1、发送数据之前不需要建立链接
2、不对数据包的顺序进行检查
3、没有错误检测和重传机制
服务对象
主要用于“查询—应答”的服务
如:NFS、NTP、DNS等
无线网卡,有线网卡,虚拟网卡。。
又称为网络适配器或网络接口卡NIC,但是现在更多的人愿意使用更为简单的名称“网卡”
通过网卡能够使不同的计算机之间连接,从而完成数据通信等功能
MAC地址,用于标识网络设备,类似于身份证号,且理论上全球唯一
组成:以太网内的MAC地址是一个48bit的值
用来标识主机或网卡的一个虚拟IP
IP地址是一种Internet上的主机编址方式,也称为网际协议地址
1.3.3.1 ip地址组成
使用32bit(IPV4),由{子网ID,主机ID}两部分组成
1.3.3.2 ip地址特点
1.3.3.3 ip地址分类如下:
A,B,C三类地址是最常用的
1.3.3.4回环ip地址
功能
主要是测试本机的网络配置,能ping通127.0.0.1说明本机的网卡和IP协议安装都没有问题
注意
127.0.0.1~127.255.255.254中的任何地址都将回环到本地主机中
不属于任何一个有类别地址类,它代表设备的本地虚拟接口
1.3.3.5 ip地址设置
自动获取网络参数(DHCP)
在局域网内会有1台主机负责管理所有的计算机网络参数,当PC启动时就会主动向服务器要求IP参数,若PC获取到服务器给的网络相关参数,那么就可以使用网络功能了进行通信
windows中:如图
在linux中重新获取ip的方式:
sudo dhclient
通过拨号取得
向ISP申请注册,直接拨到ISP,ISP会自动设置电脑的正确的网络参数
windows:如图
linux:
sudo pppoeconf
sudo pon dsl-provider //拨号ADSL
sudo poff //断开ADSL
子网掩码(subnet mask)又叫网络掩码、地址掩码是一个32bit由1和0组成的数值,并且1和0分别连续
指明IP地址中哪些位标识的是主机所在的子网以及哪些位标识的是主机号
必须结合IP地址一起使用,不能单独存在
IP地址中由子网掩码中1覆盖的连续位为子网ID,其余为主机ID
子网掩码的表现形式
192.168.220.0/255.255.255.0
192.168.220.0/24
手动进行配置如下(linux)
1.3.5.1 端口概述
TCP/IP协议采用端口标识通信的进程用于区分一个系统里的多个进程(应用程序)
特点
1、对于同一个端口,在不同系统中对应着不同的进程
2、对于同一个系统,一个端口只能被一个进程拥有
3、一个进程拥有一个端口后,传输层送到该端口的数据全部被该进程接收,同样,进程送交传输层的数据也通过该端口被送出
1.3.5.2端口号
类似pid标识一个进程;在网络程序中,用端口号(port)来标识一个运行的网络程序
特点
1、端口号是无符号短整型的类型
2、每个端口都拥有一个端口号
3、TCP、UDP维护各自独立的端口号
4、网络应用程序,至少要占用一个端口号,也可以占有多个端口号
想一想 为什么有了pid,还需要端口来标识一个进程呢?——应用程序启动一下进程号就会变
注意
端口号类似于进程号,同一时刻只能标志一个进程
可以重复使用
注意
1、IEEE802.2/802.3封装常用在无线
2、以太网封装常用在有线局域网
电话系统服务模式的抽象
每一次完整的数据传输都要经过建立连接、使用连接、终止连接的过程
本质上,连接是一个管道,收发数据不但顺序一致,而且内容相同
保证数据传输的可靠性(出错重传)
邮件系统服务模式的抽象
每个分组都携带完整的目的地址
不能保证分组的先后顺序
不进行分组出错的恢复和重传
不保证数据传输的可靠性
无论采用面向连接的还是无连接,两个进程通信过程中,大多采用C/S架构
client向server发出请求,server接收到后提供相应的服务
在通信过程中往往都是client先发送请求,而server等待请求然后进行服务
server工作过程
打开一通信通道并告知本地主机,它愿意在一特定端口(如80)上接收客户请求
等待客户请求到达该端口
接收客户请求,并发送应答信号,激活一新的线程处理这个客户请求
服务完成后,关闭新线程与客户的通信链路
client工作过程
打开一通信通道并连接到服务器特定端口
向服务器发出服务请求,等待并接收应答
根据需要继续提出请求
请求结束后关闭通信通道并终止
字节序概念
是指多字节数据的存储顺序
分类
注意
想一想
怎样确定主机的字节序?
确定主机字节序程序
#include
union{
short s;
char c[sizeof(short)];
}un;
int main()
{
un.s =0x0102;
if((un.c[0]==1)&&(un.c[1]==2))
{
printf("big-endian\n");
}
if((un.c[0]==2)&&(un.c[1]==1))
{
printf("little-endian\n");
}
return 0;
}
特点
1、网络协议指定了通讯字节序—大端
2、只有在多字节数据处理时才需要考虑字节序
3、运行在同一台计算机上的进程相互通信时,一般不用考虑字节序
4、异构计算机之间通讯,需要转换自己的字节序为网络字节序
在需要字节序转换的时候一般调用特定字节序转换函数
uint32_t htonl(uint32_t hostint32);
功能:
将32位主机字节序数据转换成网络字节序数据
参数:
hostint32:待转换的32位主机字节序数据
返回值:
成功:返回网络字节序的值
头文件:
#include
例如
#include
#include
int main(int argc, char *argv[])
{
int num = 0x01020304;//
short a = 0x0102;
int sum = htonl(num);
printf("%x\n",sum);
short b = htons(a);
printf("%x\n",b);
return 0;
}
uint16_t htons(uint16_t hostint16);
功能:
将16位主机字节序数据转换成网络字节序数据
参数:
uint16_t:unsigned short int
hostint16:待转换的16位主机字节序数据
返回值:
成功:返回网络字节序的值
头文件:
#include
uint32_t ntohl(uint32_t netint32);
功能:
将32位网络字节序数据转换成主机字节序数据
参数:
uint32_t: unsigned int
netint32:待转换的32位网络字节序数据
返回值:
成功:返回主机字节序的值
头文件:
#include
#include
#include
int main(int argc, char *argv[])
{
int num = 0x01020304;//
int sum = htonl(num);
printf("%x\n",ntohl(sum));
return 0;
}
uint16_t ntohs(uint16_t netint16);
功能:
将16位网络字节序数据转换成主机字节序数据
参数:
uint16_t: unsigned short int
netint16:待转换的16位网络字节序数据
返回值:
成功:返回主机字节序的值
头文件:
#include
#include
#include
int main(int argc, char *argv[])
{
short a = 0x0102;
short b = htons(a);
printf("%x\n",ntohs(b));
return 0;
}
2.1.6.1 inet_pton函数
字符串ip地址转整型数据
int inet_pton(int family,const char *strptr, void *addrptr);
功能:
将点分十进制数串转换成32位无符号整数
参数:
family 协议族 选IPV4对应的宏AF_INET
strptr 点分十进制数串
addrptr 32位无符号整数的地址
返回值:
成功返回1 、 失败返回其它
头文件:
#include
例:
#include
#include
int main(int argc,char *argv[])
{
char ip_str[] = "10.0.13.100";
unsigned int ip_uint = 0;
unsigned char * ip_p =NULL;//可以用char吗?
inet_pton(AF_INET,ip_str,&ip_uint);
printf("ip_uint = %d\n",ip_uint);
ip_p = (unsigned char *) &ip_uint;
printf("ip_uint = %d.%d.%d.%d\n",*ip_p,*(ip_p+1),*(ip_p+2),*(ip_p+3));
return 0;
}
运行结果:
2.1.6.2inet_ntop函数
整型数据转字符串格式ip地址
const char *inet_ntop(int family, const void *addrptr,char *strptr, size_t len);
功能:
将32位无符号整数转换成点分十进制数串
参数:
family 协议族
addrptr 32位无符号整数
strptr 点分十进制数串
len strptr缓存区长度
len的宏定义
#define INET_ADDRSTRLEN 16 //for ipv4
#define INET6_ADDRSTRLEN 46 //for ipv6
返回值:
成功:则返回字符串的首地址
失败:返回NULL
头文件:
#include
例:
#include
#include
int main()
{
unsigned char ip[]={10,0,13,252};
char ip_str[16];
inet_ntop(AF_INET,(unsigned int *)ip,ip_str,16);
printf("ip_str = %s\n",ip_str);
return 0;
}
运行结果:
UDP协议
面向无连接的用户数据报协议,在传输数据前不需要先建立连接;目地主机的运输层收到UDP报文后,不需要给出任何确认
UDP特点
1、相比TCP速度稍快些
2、简单的请求/应答应用程序可以使用UDP
3、对于海量数据传输不应该使用UDP
4、广播和多播应用必须使用UDP
UDP应用
DNS(域名解析)、NFS(网络文件系统)、RTP(流媒体)等
网络通信要解决的是不同主机进程间的通信
1、首要问题是网络间进程标识问题
2、以及多重协议的识别问题
20世纪80年代初,加州大学Berkeley分校在BSD(一个UNIX OS版本)系统内实现了TCP/IP协议;其网络程序编程开发接口为socket
随着UNIX以及类UNIX操作系统的广泛应用, socket成为最流行的网络程序开发接口
socket作用
提供不同主机上的进程之间的通信
socket特点
1、socket也称“套接字”
2、是一种文件描述符,代表了一个通信管道的一个端点
3、类似对文件的操作一样,可以使用read、write、close等函数对socket套接字进行网络数据的收取和发送等操作
4、得到socket套接字(描述符)的方法调用socket()
int socket(int family,int type,int protocol);
功能:
创建一个用于网络通信的socket套接字(描述符)
参数:
family:协议族(AF_INET、AF_INET6、PF_PACKET等)
/流式套接字 用于TCP通信 /报式套接字 用于UDP通信
type:套接字类( SOCK_STREAM、 SOCK_DGRAM、 SOCK_RAW等)
protocol:协议类别(0、IPPROTO_TCP、IPPROTO_UDP等)
/0自动指定协议
返回值:
套接字
特点:
创建套接字时,系统不会分配端口
创建的套接字默认属性是主动的,即主动发起服务的请求;当作为服务器时,往往需要修改为被动的
头文件:
#include
int sockfd = 0;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socket");
exit(-1);
}
注意:
AF_INET:IPv4协议
SOCK_DGRAM:数据报套接字
0:选择所给定的family和type组合的系统默认值
头文件:#include <netinet/in.h>
struct in_addr
{
in_addr_t s_addr;//4字节
};
struct sockaddr_in
{
sa_family_t sin_family;//2字节
in_port_t sin_port;//2字节
struct in_addr sin_addr;//4字节
char sin_zero[8]//8字节
};
为了使不同格式地址能被传入套接字函数,地址须要强制转换成通用套接字地址结构
头文件:#include <netinet/in.h>
struct sockaddr
{
sa_family_t sa_family; // 2字节
char sa_data[14] //14字节
};
注意:
以上3个结构在linux系统中已经定义
在定义源地址和目的地址结构的时候,选用struct sockaddr_in;
例:
struct sockaddr_in my_addr;
当调用编程接口函数,且该函数需要传入地址结构时需要用struct sockaddr进行强制转换
例:
bind(sockfd,(struct sockaddr*)&my_addr,sizeof(my_addr));
ssize_t sendto(int sockfd,
const void *buf,
size_t nbytes,
int flags,
const struct sockaddr *to,
socklen_t addrlen);
功能:
向to结构体指针中指定的ip,发送UDP数据
参数:
sockfd:套接字
buf: 发送数据的地址
nbytes: 发送数据的大小
flags: 一般为0
to: 指向目的主机地址结构体的指针
addrlen:to所指向内容的长度
注意:
通过to和addrlen确定目的地址
可以发送0长度的UDP数据包
返回值:
成功:发送数据的字符数
失败: -1
例:udp_send.c
#include
#include
#include
#include
#include
#include
#include
int main(int argc,char *argv[])
{
unsigned short port = 8080;
char *server_ip = "10.0.31.245";
if(argc > 1)//通过main函数传参,传入ip地址
{
server_ip = argv[1];
}
if(argc > 2)//通过main函数传参,传入端口号
{
port = atoi(argv[2]);
}
/*创建UDP套接字*/
int sockfd;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
perror("socket");
exit(-1);
}
/*填充目的server socket地址*/
struct sockaddr_in dest_addr;
bzero(&dest_addr,sizeof(dest_addr));
dest_addr.sin_family = AF_INET;//目的套接字地址的协议家族赋值
dest_addr.sin_port = htons(port);//目的套接字地址的端口号赋值
inet_pton(AF_INET,server_ip,&dest_addr.sin_addr);//目的套接字地址的ip地址赋值
printf("send data to UDP server %s:%d!\n",server_ip,port);
/*发送数据到目的server*/
while(1)
{
char send_buf[512];
fgets(send_buf,sizeof(send_buf),stdin);
send_buf[strlen(send_buf)-1] = '\0';//字符串最后一个'\n'变成'\0'
sendto(sockfd,send_buf,strlen(send_buf),0,(struct sockaddr *)&dest_addr,sizeof(dest_addr));
}
close(sockfd);
return 0;
}
UDP网络程序想要收取数据需什么条件?
确定的ip地址
确定的port
怎样完成上面的条件呢?
接收端 使用bind函数,来完成地址结构与socket套接字的绑定,这样ip、port就固定了
发送端 在sendto函数中指定接收端的ip、port,就可以发送数据了
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
功能:
将本地协议地址与sockfd绑定
参数:
sockfd: socket套接字
myaddr: 指向特定协议的地址结构指针
addrlen:该地址结构的长度
返回值:
成功:返回0
失败:其他
注意:
INADDR_ANY 通配地址,值为0
头文件:
#include
ssize_t recvfrom(int sockfd, void *buf,size_t nbytes,int flags,
struct sockaddr *from,socklen_t *addrlen);
功能:
接收UDP数据,并将源地址信息保存在from指向的结构中
参数:
sockfd: 套接字
buf: 接收数据缓冲区地址
nbytes: 接收数据缓冲区的大小
flags: 套接字标志(常为0)
from: 源地址结构体指针,用来保存数据的来源
addrlen: from所指内容的长度
注意:
通过from和addrlen参数存放数据来源信息
from和addrlen可以为NULL, 表示不保存数据来源
返回值:
成功:接收到的字符数
失败: -1
想一想:
上节中的2个demo中发送数据端其实就是client;接收数据端就是server,那么client能否接收数据?server能否发送数据呢?
答:
其实在网络编程开发中client和server双方既可以有发送数据还可以接收数据;一般认为提供服务的一方为server;而接受服务的另一方为client
#include
#include
#include
int main(int argc, char *argv[])
{
//创建套接字
int sock_fd = socket(AF_INET,SOCK_DGRAM,0);
if(sock_fd < 0)
perror("");
//绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9090);
//inet_pton(AF_INET,"192.168.127.129",&addr.sin_addr.s_addr);
addr.sin_addr.s_addr =0;// 通配地址 主机上所有IP都绑定套接字
int ret = bind(sock_fd,(struct sockaddr*)&addr,sizeof(addr));
if(ret < 0)
perror("");
struct sockaddr_in cli_addr;
socklen_t len = sizeof(cli_addr);
while(1)
{
char buf[128]="";
int n = recvfrom(sock_fd,buf,sizeof(buf),0,(struct sockaddr*)&cli_addr,&len);
printf("%s\n",buf);
sendto(sock_fd,buf,n,0,(struct sockaddr*)&cli_addr,sizeof(cli_addr));
}
close(sock_fd);
return 0;
}
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9000);
inet_pton(AF_INET,"192.168.127.1",&server_addr.sin_addr.s_addr);
//创建套接字
int sock_fd = socket(AF_INET,SOCK_DGRAM,0);
if(sock_fd< 0)
perror("");
while(1)
{
char buf[128]="";
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1]=0;
sendto(sock_fd,buf,strlen(buf),0,(struct sockaddr*) &server_addr,sizeof(server_addr));
char read_buf[128]="";
recvfrom(sock_fd,read_buf,sizeof(read_buf),0,NULL,NULL);
printf("%s\n",read_buf);
}
return 0;
}
视频里有讲
1、本地IP、本地端口(我是谁)
2、目的IP、目的端口(发给谁)
3、在客户端的代码中,我们只设置了目的IP、目的端口
客户端的本地ip、本地port是我们调用sendto的时候linux系统底层自动给客户端分配的;分配端口的方式为随机分配,即每次运行系统给的port不一样
1、服务器之所以要bind是因为它的本地port需要是固定,而不是随机的
2、服务器也可以主动地给客户端发送数据
3、客户端也可以用bind,这样客户端的本地端口就是固定的了,但一般不这样做
TFTP:简单文件传送协议(默认端口号:69)
最初用于引导无盘系统,被设计用来传输小文件(一般用于局域网内上传下载文件,如嵌入式刷系统、网吧)
特点:
基于UDP协议实现(在receive和send函数前加上TFTP协议头)
不进行用户有效性认证
数据传输模式:
octet:二进制模式
netascii:文本模式
mail:已经不再支持
TFTP通信过程总结(无选项)
1、服务器在69号端口等待客户端的请求
2、服务器若批准此请求,则使用临时端口与客户端进行通信
3、每个数据包的编号都有变化(从1开始)
4、每个数据包都要得到ACK的确认如果出现超时,则需要重新发送最后的包(数据或ACK)
5、数据的长度以512Byte传输(不包括协议长度)
6、小于512Byte的数据意味着传输结束
想一想
TFTP协议的格式是什么样子?
例如下载服务器的a.txt文件过程:
注意:
以上的0代表的是’\0’
不同的差错码对应不同的错误信息
错误码:
0 未定义,参见错误信息
1 File not found.
2 Access violation.
3 Disk full or allocation exceeded.
4 illegal TFTP operation.
5 Unknown transfer ID.
6 File already exists.
7 No such user.
8 Unsupported option(s) requested.
想一想
传输的数据的大小一定是512Byte吗?
由于网络的原因,一方收不到另一方的数据怎么办?
TFTP通信过程总结(带选项)
1、可以通过发送带选项的读/写请求发送给server
2、如果server允许修改选项则发送选项修改确认包
3、server发送的数据、选项修改确认包都是临时port
4、server通过timeout来对丢失数据包的重新发送
练习要求
使用TFTP协议,下载server上的文件到本地
实现思路
1、构造请求报文,送至服务器(69号端口)
2、等待服务器回应
3、分析服务器回应
4、接收数据,直到接收到的数据包小于规定数据长度
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define GREEN "\e[32m" //shell打印显示绿色
#define RED "\e[31m" //shell打印显示红色
#define PRINT(X,Y) { write(1,Y,5); \
printf(X); \
fflush(stdout); \
write(1,"\e[0m",4); \
}
static int sockfd;
static struct sockaddr_in dest_addr;
void sig_dispose(int sig)
{
if(SIGINT == sig){
close(sockfd);
puts("\nclose!");
system("stty sane");//回显
exit(0);
}
}
//上传
void tftp_upload(char *argv)
{
int fd,read_len;
unsigned short p_num = 0;
unsigned char cmd = 0;
char cmd_buf[512] = "";
char recv_buf[516] = "";
struct sockaddr_in client_addr;
socklen_t cliaddr_len = sizeof(client_addr);
if(dest_addr.sin_port == 0){
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(69);
puts("send to IP:");
fgets(recv_buf,sizeof(recv_buf),stdin);
*(strchr(recv_buf,'\n')) = '\0';
inet_pton(AF_INET, recv_buf, &dest_addr.sin_addr);
}
//构造上传请求,argv为文件名
int len = sprintf(cmd_buf, "%c%c%s%c%s%c", 0, 2, argv, 0, "octet", 0); //发送读数据包请求
sendto(sockfd, cmd_buf, len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
fd = open(argv, O_RDONLY);
if(fd < 0 ){
perror("open error");
close(sockfd);
exit(-1);
}
do{
//接收服务器发送的内容
len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len);
cmd = recv_buf[1];
if( cmd == 4 ) //是否为ACK
{
p_num = ntohs(*(unsigned short*)(recv_buf+2));
read_len = read(fd, recv_buf+4, 512);
printf("recv:%d\n", p_num);//十进制方式打印包编号
recv_buf[1] = 3;//构建数据包
(*(unsigned short *)(recv_buf+2)) = htons(p_num + 1);
//printf("%s\n", recv_buf+3);
sendto(sockfd, recv_buf, read_len+4, 0, (struct sockaddr*)&client_addr, sizeof(client_addr));
}
else if( cmd == 5 ) //是否为错误应答
{
close(sockfd);
close(fd);
printf("error:%s\n", recv_buf+4);
exit(-1);
}
}while(read_len == 512); //读取的数据小于512则认为结束
len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len); //接收最后一个ACK确认包
close(fd);
PRINT("Download File is Successful\n", RED);
return;
}
//下载
void tftp_down(char *argv)
{
int fd;
unsigned short p_num = 0;
unsigned char cmd = 0;
char cmd_buf[512] = "";
char recv_buf[516] = "";
struct sockaddr_in client_addr;
socklen_t cliaddr_len = sizeof(client_addr);
if(dest_addr.sin_port == 0){
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(69);
puts("send to IP:");
fgets(recv_buf,sizeof(recv_buf),stdin);
*(strchr(recv_buf,'\n')) = '\0';
inet_pton(AF_INET, recv_buf, &dest_addr.sin_addr);
}
//构造下载请求,argv为文件名
int len = sprintf(cmd_buf, "%c%c%s%c%s%c", 0, 1, argv, 0, "octet", 0); //发送读数据包请求
sendto(sockfd, cmd_buf, len, 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
fd = open(argv, O_WRONLY|O_CREAT, 0666);
if(fd < 0 ){
perror("open error");
close(sockfd);
exit(-1);
}
do{
//接收服务器发送的内容
len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len);
cmd = recv_buf[1];
if( cmd == 3 ) //是否为数据包
{
//接收的包编号是否为上次包编号+1
if((unsigned short)(p_num+1) == ntohs(*(unsigned short*)(recv_buf+2) ))
{
write(fd, recv_buf+4, len-4);
p_num = ntohs(*(unsigned short*)(recv_buf+2));
printf("recv:%d\n", p_num);//十进制方式打印包编号
}
recv_buf[1] = 4;
sendto(sockfd, recv_buf, 4, 0, (struct sockaddr*)&client_addr, sizeof(client_addr));
}
else if( cmd == 5 ) //是否为错误应答
{
close(sockfd);
close(fd);
unlink(argv);//删除文件
printf("error:%s\n", recv_buf+4);
exit(-1);
}
}while((len == 516)||(cmd == 6)); //如果收到的数据小于516则认为出错
close(fd);
PRINT("Download File is Successful\n", RED);
return;
}
void help_fun(int argc, char *argv[])
{
printf("1 down\n");
printf("2 upload\n");
printf("3 exit\n");
return;
}
char mygetch()
{
struct termios oldt, newt;
char ch;
tcgetattr( STDIN_FILENO, &oldt );
newt = oldt;
newt.c_lflag &= ~( ICANON | ECHO );
tcsetattr( STDIN_FILENO, TCSANOW, &newt );
ch = getchar();
tcsetattr( STDIN_FILENO, TCSANOW, &oldt );
return ch;
}
int main(int argc, char *argv[])
{
char cmd_line[100];
signal(SIGINT,sig_dispose);
bzero(&dest_addr, sizeof(dest_addr));
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0){
perror("socket error");
exit(-1);
}
while(1){
int cmd;
help_fun(argc,argv);
PRINT("send>", GREEN);
cmd = mygetch();
if(cmd == '1'){
puts("input file name:");
fgets(cmd_line,sizeof(cmd_line),stdin);
*(strchr(cmd_line,'\n')) = '\0';
tftp_down(cmd_line);
}
else if(cmd == '2') {
puts("input file name:");
fgets(cmd_line,sizeof(cmd_line),stdin);
*(strchr(cmd_line,'\n')) = '\0';
tftp_upload(cmd_line);
}
else if(cmd == '3') {
close(sockfd);
system("stty sane");//回显
exit(0);
}
}
close(sockfd);
return 0;
}
广播:由一台主机向该主机所在子网内的所有主机发送数据的方式
广播只能用UDP或原始IP实现,不能用TCP
单个服务器与多个客户主机通信时减少分组流通
以下几个协议都用到广播
1、处于同一子网的所有主机都必须处理数据
2、UDP数据包会沿协议栈向上一直到UDP层
3、运行音视频等较高速率工作的应用,会带来大负载
4、局限于局域网内使用
{网络ID,主机ID}
网络ID表示由子网掩码中1覆盖的连续位
主机ID表示由子网掩码中0覆盖的连续位
定向广播地址:主机ID全1
1、例:对于192.168.220.0/24,其定向广播地址为192.168.220.255(所有主机必须无条件接收)
2、通常路由器不转发该广播
受限广播地址:255.255.255.255
路由器从不转发该广播(否则全世界的人都接收了)
单播:
广播:
主机ID全1,即IP地址最后一位255,所有IP都要接收
MAC地址全FF,所有链路层MAC都要接收
套接口选项
int setsockopt(int sockfd, int level,
int optname,const void *optval,
socklen_t optlen);
成功执行返回0,否则返回-1
参数说明:
多播:
数据的收发仅仅在同一分组中进行
多播的特点:
1、多播地址标示一组接口
2、多播可以用于广域网使用
3、在IPv4中,多播是可选的
IPv4的D类IP地址是多播地址
十进制:224.0.0.1 ~ 239.255.255.254(里面任意一个都是多播地址)
十六进制:E0.00.00.01 ~ EF.FF.FF.FE
多播地址向以太网MAC地址的映射(MAC地址不能直接全部设为FF,需要进行过滤)
多播地址结构体
在IPv4因特网域(AF_INET)中,多播地址结构体用如下结构体ip_mreq表示:
结构体ip_mreq里面的两个IP分别表示 :
多播组的IP
自己的IP
多播套接口选项
int setsockopt(int sockfd, int level,int optname,
const void *optval, socklen_t optlen);
成功执行返回0,否则返回-1
TCP回顾
1、面向连接的流式协议;可靠、出错重传、且每收到一个数据都要给出相应的确认
2、通信之前需要建立链接
3、服务器被动链接,客户端是主动链接
做为客户端需要具备的条件
1、知道“服务器”的ip、port
2、主动连接“服务器”
3、需要用到的函数
socket—创建“主动TCP套接字”
connect—连接“服务器”
send—发送数据到“服务器”
recv—接受“服务器”的响应
close—关闭连接
connect函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t len);
功能:
主动跟服务器建立链接
参数:
sockfd:socket套接字
addr: 连接的服务器地址结构
len: 地址结构体长度
返回值:
成功:0 失败:其他
注意:
1、connect建立连接之后不会产生新的套接字
2、连接成功后才可以开始传输TCP数据
3、头文件:#include
send函数
ssize_t send(int sockfd, const void* buf,size_t nbytes, int flags);
功能:
用于发送数据
参数:
sockfd: 已建立连接的套接字
buf: 发送数据的地址
nbytes: 发送缓数据的大小(以字节为单位)
flags: 套接字标志(常为0)
返回值:
成功发送的字节数
头文件:
#include
注意:
不能用TCP协议发送0长度的数据包
发送数据如图所示
recv函数
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);
功能:
用于接收网络数据
参数:
sockfd:套接字
buf: 接收网络数据的缓冲区的地址
nbytes: 接收缓冲区的大小(以字节为单位)
flags: 套接字标志(常为0)
返回值:
成功接收到字节数
头文件:
#include
接收数据如图所示:
#include
#include
#include
#include
#include
int main(int argc, char *argv[])
{
//创建流式套接字
int s_fd = socket(AF_INET,SOCK_STREAM,0);
if(s_fd < 0)
perror("");
//连接服务器
struct sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = htons(8080);//服务器的端口
ser_addr.sin_addr.s_addr = inet_addr("192.168.127.1");//服务器的ip
int ret = connect(s_fd, (struct sockaddr*)&ser_addr,sizeof(ser_addr));
if(ret<0)
perror("");
//收发数据
while(1)
{
char buf[128]="";
char r_buf[128]="";
fgets(buf,sizeof(buf),stdin);
buf[strlen(buf)-1]=0;//最后一个字符置零
write(s_fd,buf,strlen(buf));
int cont = read(s_fd,r_buf,sizeof(r_buf));
if(cont == 0)
{
printf("server close\n");
break;//对方关闭!!
}
printf("recv server = %s\n",r_buf);
}
//关闭套接字'
close(s_fd);
return 0;
}
做为TCP服务器需要具备的条件
1、具备一个可以确知的地址
2、让操作系统知道是一个服务器,而不是客户端
3、等待连接的到来
对于面向连接的TCP协议来说,连接的建立才真正意味着数据通信的开始
listen 函数
int listen(int sockfd, int backlog);
功能:
将套接字由主动修改为被动
使操作系统为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接
参数:
sockfd: socket监听套接字
backlog:已完成连接队列和未完成连接队列上连接数之和的最大值 一般写128
返回值:
成功:返回0
失败:其他
头文件:
#include
accept函数
int accept(int sockfd,struct sockaddr *cliaddr, socklen_t *addrlen);
功能:
从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待(阻塞)
参数:
sockfd: socket监听套接字
cliaddr: 用于存放客户端套接字地址结构
addrlen:套接字地址结构体长度的地址
返回值:
已连接套接字
头文件:
#include
注意:
返回的是一个已连接套接字,这个套接字代表当前这个连接
close 关闭套接字
1、使用close函数即可关闭套接字
关闭一个代表已连接套接字将导致另一端接收到一个0长度的数据包
2、做服务器时
1>关闭监听套接字将导致服务器无法接收新的连接,但不会影响已经建立的连接
2>关闭accept返回的已连接套接字将导致它所代表的连接被关闭,但不会影响服务器的监听
3、做客户端时
关闭连接就是关闭连接,不意味着其他
#include
#include
#include
#include
int main(int argc, char *argv[])
{
//创建套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd < 0)
perror("");
//绑定
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(9999);
myaddr.sin_addr.s_addr = 0;//本机的ip都绑定
bind(lfd,(struct sockaddr*)&myaddr,sizeof(myaddr));//绑定
//监听
listen(lfd,128);
//提取
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
int cfd = accept(lfd,(struct sockaddr *)&cliaddr,&len);//提取
if(cfd < 0 )
perror("");
char ip[16]="";
printf("client ip=%s port=%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),ntohs(cliaddr.sin_port));
//读写
while(1)
{
char buf[256]="";
//回射服务器
int ret = read(cfd,buf,sizeof(buf));
if(ret == 0)
{
printf("client close\n");
break;
}
printf("recv = [%s]\n",buf);
write(cfd,buf,ret);
}
//关闭
close(lfd);
close(cfd);
return 0;
}
注意:这个服务器是有问题的,强制退出再运行,客户端连接不上了!因为绑定的端口号没有被释放,后面章节会讲这个。
三次握手开始于客户端调用connect函数
断开连接的时候,谁主动关闭谁就要等待2MSL,所以再次连接就会连接不上,因为端口被占用了。这里可以设置端口复用
accept和read函数默认是带阻塞的,必须要多进程或多线程实现并发通信。
#include
#include
#include
#include
#include
#include
#include
#include
void cath_child(int num)
{
pid_t pid;
while(1)
{ //-1是任意子进程 不接收子进程返回参数
pid = waitpid(-1,NULL,WNOHANG);//循环回收子进程资源,不然浪费了
if(pid <= 0)//都回收完了
{
break;
}
else if(pid > 0)//回收到了,继续回收
{
printf("child process %d\n",pid);
continue;
}
}
}
int main(int argc, char *argv[])
{
//将SIGCHLD 加入到阻塞集中
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_BLOCK,&set,NULL);//信号上锁,屏蔽信号
// 创建套接字
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd <0)
perror("");
// 绑定
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(6666);
myaddr.sin_addr.s_addr = 0;
bind(lfd,(struct sockaddr *)&myaddr,sizeof(myaddr));
// 监听
listen(lfd,128);
// 提取
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
while(1)
{
int cfd = accept(lfd,(struct sockaddr*)&cliaddr,&len);
if(cfd < 0)
perror("");
char ip[16]="";
printf("client ip=%s port=%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
ntohs(cliaddr.sin_port));
pid_t pid;
pid = fork();
if(pid ==0 )//子进程
{
close(lfd);
while(1)
{
sleep(1);
char buf[256]="";
int n = read(cfd,buf,sizeof(buf));
if(0==n)
{
printf("client close\n");
break;
}
printf("[*********%s*********]\n",buf);
write(cfd,buf,n);
}
//close(cfd);
//exit(0);
break;
}
else if(pid > 0)//父进程
{
close(cfd);
struct sigaction act;
act.sa_handler = cath_child;//信号回调函数
act.sa_flags = 0;//一般置零
sigemptyset(&act.sa_mask);//请空mask(捕捉到信号后临时的一个屏蔽集)
sigaction(SIGCHLD,&act,NULL);
sigprocmask(SIG_UNBLOCK,&set,NULL);
}
// 创建子进程
}
// 收尾
return 0;
}
注意:上述代码中,accept函数默认阻塞等待新的连接,客户端退出时,accept被信号中断和软件层次中断,后面接着运行了,运行会一直打印**********,所以需要对accept返回值进行判断。代码如下(使用视频里面的包裹函数,对各个函数返回值进行判断处理):
wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
#include
#include
#include
#include
#include
#include
#include
#include
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
int tcp4bind(short port,const char *IP);
#endif
wrap.c
#include
#include
#include
#include
#include
#include
#include
#include
void perr_exit(const char *s)
{
perror(s);
exit(-1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ((n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))//如果是被信号中断和软件层次中断,不能退出
goto again;//继续返回到accept阻塞 等待新的客户端接入
else
perr_exit("accept error");
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = connect(fd, sa, salen)) < 0)
perr_exit("connect error");
return n;
}
int Listen(int fd, int backlog)
{
int n;
if ((n = listen(fd, backlog)) < 0)
perr_exit("listen error");
return n;
}
int Socket(int family, int type, int protocol)
{
int n;
if ((n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {//默认是阻塞的
if (errno == EINTR)//如果是被信号中断,不应该退出
goto again;//继续回去读
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
/*参三: 应该读取固定的字节数数据*/
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft; //usigned int 剩余未读取的字节数
ssize_t nread; //int 实际读到的字节数
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
/*:写固定的字节数数据*/
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
//读一行,遇到\0就结束
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr = 0;
return n;
}
//传端口和IP参数即可,很方便
int tcp4bind(short port,const char *IP)
{
struct sockaddr_in serv_addr;
int lfd = Socket(AF_INET,SOCK_STREAM,0);
bzero(&serv_addr,sizeof(serv_addr));
if(IP == NULL){
//如果这样使用 0.0.0.0,任意ip将可以连接
serv_addr.sin_addr.s_addr = INADDR_ANY;
}else{
if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
perror(IP);//转换失败
exit(1);
}
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
// int opt = 1;
//setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
return lfd;
}
#include
#include
#include
#include
#include
#include
#include
typedef struct _info
{
int cfd;
struct sockaddr_in client;
}INFO;
void* resq_client(void *arg)
{
INFO *info = (INFO*)arg;
char ip[16]="";
printf("client ip=%s port=%d\n",
inet_ntop(AF_INET,&(info->client.sin_addr.s_addr),ip,16),
ntohs(info->client.sin_port));
while(1)
{
char buf[1024]="";
int n = read(info->cfd,buf,sizeof(buf));
if(n == 0)
{
printf("cleint close\n");
break;
}
printf("%s\n",buf);
write(info->cfd,buf,n);
}
close(info->cfd);
free(info);
}
int main(int argc, char *argv[])
{
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd <0)
perror("");
int opt=1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in myaddr;
myaddr.sin_family = AF_INET ;
myaddr.sin_port= htons(8888);
myaddr.sin_addr.s_addr = 0;
if( bind(lfd,(struct sockaddr*)&myaddr,sizeof(myaddr))< 0)
perror("bind:");
listen(lfd,128);
while(1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int cfd = accept(lfd,(struct sockaddr*)&client,&len);
INFO *info = malloc(sizeof(INFO));//接入一个客户端线程就开辟一段内存,防止线程间相互干扰
info->cfd =cfd;
info->client = client;
pthread_t pthid; //传参数 结构体的地址
pthread_create(&pthid,NULL,resq_client,info);
pthread_detach(pthid);//线程脱离
}
close(lfd);
return 0;
}
功能:监听文件描述符的属性变化(读写缓冲区的变化)
#include
/* According to earlier standards */
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
函数功能: 监听多个文件描述符的属性变化(读写缓冲区的变化)
参数:
nfds: 监听的最大文件描述符+1 (遍历的文件描述符个数)
readfds: 需要监听读属性变化的文件描述符集合
writefds: 需要监听写属性变化的文件描述符集合
exceptfds:需要监听异常属性变化的文件描述符集合
timeout: 多久监听一次,超时时间
NULL 永久监听
>0 超时时间
返回值: 监听到变化的文件描述符个数
void FD_CLR(int fd, fd_set *set);//将文件描述符fd从set集合中清除
int FD_ISSET(int fd, fd_set *set);//判断文件描述符fd是否在set集合中
void FD_SET(int fd, fd_set *set);//将文件描述符fd加入到set集合中
void FD_ZERO(fd_set *set);//清空set文件描述符集合
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
文件描述符0、1、2(前三个是标准描述符)、3…逐步递增的
select监听的流程
创建套接字
绑定
监听
while(1){
select()//监听lfd
}
05 select_tcp_server.c
#include
#include "wrap.h"
#include
#include
#include
int main(int argc, char *argv[])
{
//创建套接字
//绑定
int lfd = tcp4bind(9999,NULL);
//监听
listen(lfd,128);
int max_fd = lfd;
fd_set r_set;//需要监听的文件描述符集合
fd_set old_set;//备份
FD_ZERO(&old_set);
FD_ZERO(&r_set);
FD_SET(lfd,&old_set);
int nready=0;
while(1)
{
r_set = old_set;
nready = select(max_fd+1,&r_set,NULL,NULL,NULL);//监听变化的个数
if(nready < 0)
{
perror("");
break;
}
else if(nready >= 0)
{
//判断lfd是否变化了(是否在set集合内)
if(FD_ISSET(lfd,&r_set))
{
// 提取新的连接
struct sockaddr_in cliaddr;
socklen_t len=sizeof(cliaddr);
char ip[16]="";
int cfd = Accept(lfd,(struct sockaddr*)&cliaddr,&len);
printf("client ip=%s port=%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
ntohs(cliaddr.sin_port));
//将cfd加入到old_set
FD_SET(cfd,&old_set);
//更新最大值
if(max_fd < cfd)
max_fd = cfd;
//如果只有lfd变化,执行下一次监听
if( --nready == 0)
continue;//下面循环先不进行
}
for(int i=lfd+1;i<=max_fd;i++)//遍历文件描述符 看哪个变化的
{
//cfd变化了
if(FD_ISSET(i,&r_set))
{
char buf[1024]="";
int n = Read(i,buf,sizeof(buf));
if(n == 0)
{
printf("client close\n");
close(i);
//将i从old_set删除,不监听了
FD_CLR(i,&old_set);
}
else if(n < 0)
{
perror("");
}
else
{
printf("%s\n",buf);
Write(i,buf,n);
}
}
}
}
}
return 0;
}
优点:
跨平台 windows,linux下都可以使用(主要是windows平台用,linux不用)
并发量高
效率高
消耗资源少
消耗cpu也少
缺点:
问题:
select未优化之前的代码:
#include
#include
#include
#include
#include "wrap.h"
#include
#define PORT 8888
int main(int argc, char *argv[])
{
//创建套接字,绑定
int lfd = tcp4bind(PORT,NULL);
//监听
Listen(lfd,128);
int maxfd = lfd;//最大的文件描述符
fd_set oldset,rset;
FD_ZERO(&oldset);
FD_ZERO(&rset);
//将lfd添加到oldset集合中
FD_SET(lfd,&oldset);
while(1)
{
rset = oldset;//将oldset赋值给需要监听的集合rset
int n = select(maxfd+1,&rset,NULL,NULL,NULL);
if(n < 0)
{
perror("");
break;
}
else if(n == 0)
{
continue;//如果没有变化,重新监听
}
else//监听到了文件描述符的变化
{
//lfd变化 代表有新的连接到来
if( FD_ISSET(lfd,&rset))
{
struct sockaddr_in cliaddr;
socklen_t len =sizeof(cliaddr);
char ip[16]="";
//提取新的连接
int cfd = Accept(lfd,(struct sockaddr*)&cliaddr,&len);
printf("new client ip=%s port=%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
ntohs(cliaddr.sin_port));
//将cfd添加至oldset集合中,以下次监听
FD_SET(cfd,&oldset);
//更新maxfd
if(cfd > maxfd)
maxfd = cfd;
//如果只有lfd变化,continue
if(--n == 0)
continue;
}
//cfd 遍历lfd之后的文件描述符是否在rset集合中,如果在则cfd变化
for(int i = lfd+1;i<=maxfd;i++)
{
//如果i文件描述符在rset集合中
if(FD_ISSET(i,&rset))
{
char buf[1500]="";
int ret = Read(i,buf,sizeof(buf));
if(ret < 0)//出错,将cfd关闭,从oldset中删除cfd
{
perror("");
close(i);
FD_CLR(i,&oldset);
continue;
}
else if(ret == 0)
{
printf("client close\n");
close(i);
FD_CLR(i,&oldset);
}
else
{
printf("%s\n",buf);
Write(i,buf,ret);
}
}
}
}
}
return 0;
}
优化select
两种方法:
数组法优化:
//进阶版select,通过数组防止遍历1024个描述符
#include
#include
#include
#include
#include
#include
#include "wrap.h"
#define SERV_PORT 8888
int main(int argc, char *argv[])
{
int i, j, n, maxi;
int nready, client[FD_SETSIZE]; /* 自定义数组client, 防止遍历1024个文件描述符 FD_SETSIZE默认为1024 */
int maxfd, listenfd, connfd, sockfd;
char buf[BUFSIZ], str[INET_ADDRSTRLEN]; /* #define INET_ADDRSTRLEN 16 */
struct sockaddr_in clie_addr, serv_addr;
socklen_t clie_addr_len;
fd_set rset, allset; /* rset 读事件文件描述符集合 allset用来暂存 */
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
//端口复用
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family= AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port= htons(SERV_PORT);
Bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listenfd, 128);
maxfd = listenfd; /* 起初 listenfd 即为最大文件描述符 */
maxi = -1; /* 将来用作client[]的下标, 初始值指向0个元素之前下标位置 */
for (i = 0; i < FD_SETSIZE; i++)
client[i] = -1; /* 用-1初始化client[] */
FD_ZERO(&allset);
FD_SET(listenfd, &allset); /* 构造select监控文件描述符集 */
while (1) {
rset = allset; /* 每次循环时都从新设置select监控信号集 */
nready = select(maxfd+1, &rset, NULL, NULL, NULL); //2 1--lfd 1--connfd
if (nready < 0)
perr_exit("select error");
if (FD_ISSET(listenfd, &rset)) { /* 说明有新的客户端链接请求 */
clie_addr_len = sizeof(clie_addr);
connfd = Accept(listenfd, (struct sockaddr *)&clie_addr, &clie_addr_len); /* Accept 不会阻塞 */
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &clie_addr.sin_addr, str, sizeof(str)),
ntohs(clie_addr.sin_port));
for (i = 0; i < FD_SETSIZE; i++)
if (client[i] < 0) { /* 找client[]中没有使用的位置 */
client[i] = connfd; /* 保存accept返回的文件描述符到client[]里 */
break;
}
if (i == FD_SETSIZE) { /* 达到select能监控的文件个数上限 1024 */
fputs("too many clients\n", stderr);
exit(1);
}
FD_SET(connfd, &allset); /* 向监控文件描述符集合allset添加新的文件描述符connfd */
if (connfd > maxfd)
maxfd = connfd; /* select第一个参数需要 */
if (i > maxi)
maxi = i; /* 保证maxi存的总是client[]最后一个元素下标 */
if (--nready == 0)
continue;
}
for (i = 0; i <= maxi; i++) { /* 检测哪个clients 有数据就绪 */
if ((sockfd = client[i]) < 0)
continue;//数组内的文件描述符如果被释放有可能变成-1
if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, sizeof(buf))) == 0) { /* 当client关闭链接时,服务器端也关闭对应链接 */
Close(sockfd);
FD_CLR(sockfd, &allset); /* 解除select对此文件描述符的监控 */
client[i] = -1;
} else if (n > 0) {
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
Write(sockfd, buf, n);
Write(STDOUT_FILENO, buf, n);
}
if (--nready == 0)
break; /* 跳出for, 但还在while中 */
}
}
}
Close(listenfd);
return 0;
}
第二种优化的方法:
使用文件描述符的重定向
将关闭的文件描述符指向maxfd,在将maxfd关闭
阿里的服务器用的就是epoll,epoll的并发量和你的电脑的性能有关系,一般个人电脑并发量一万左右。
1 创建红黑树,用于监听文件描述符
#include
int epoll_create(int size);
功能: 创建一棵红黑树
参数:
size : 写大于0即可(内核自动扩展节点个数)
返回值: 树的句柄
2 节点上树 节点下树 修改树节点
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能: 节点上树 节点下树 修改树节点
参数:
epfd : 树的根节点(句柄)
op:
EPOLL_CTL_ADD: 上树
EPOLL_CTL_MOD: 修改(比如将读事件修改成写事件)
EPOLL_CTL_DEL: 下树
fd: 上树,下树,修改的文件描述符
event :
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events:
EPOLLIN 读事件
EPOLLOUT 写事件
typedef union epoll_data {
void *ptr;//反应堆,下一节讲
int fd;// 需要监听的文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
返回值:
成功返回0 失败返回-1
3 监听
#include
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
功能: 监听树上的节点属性变化
参数:
epfd: 树的根节点
events: struct epoll_event数组首元素的地址
maxevents: 数组元素的个数
timeout: -1 永久监听 >=0 限时等待
返回值: 变化文件描述符的个数
01epoll_tcp_server.c
#include "wrap.h"
#include
int main(int argc, char const *argv[])
{
// 创建套接字
// 绑定
int lfd = tcp4bind(8888,NULL);
//监听
listen(lfd,128);
// 创建树
int epfd = epoll_create(1);
//将lfd上树
struct epoll_event ev,evs[1024];
ev.events = EPOLLIN;//读事件
ev.data.fd = lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
//循环监听树
while(1)
{
int n = epoll_wait(epfd,evs,1024,-1);
if(n < 0)
{
perr_exit("epoll");//包裹函数封装,错误退出
}
else if(n >= 0)
{
for(int i=0;i<n;i++)//遍历数组
{
int fd = evs[i].data.fd;
//如果是lfd变化,并且是读事件变化 用位与判断,不用管其他位数据
if(fd == lfd &&evs[i].events&EPOLLIN)
{
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
char ip[16]="";
int cfd = Accept(lfd,(struct sockaddr*)&cliaddr,&len);
printf("client ip=%s port=%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
ntohs(cliaddr.sin_port));
//将cfd上树
ev.data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
}
//是cfd变化 并且是读事件
else if(evs[i].events&EPOLLIN)
{
char buf[1500]="";
int count = Read(fd,buf,sizeof(buf));
if(count <= 0)
{
printf("error or client close\n");
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&evs[i]);//下树
}
else
{
printf("%s\n",buf);
Write(fd,buf,count);
}
}
}
}
}
return 0;
}
注意: 因为监听写缓冲区时,如果时监听水平触发事件,只要可写就会被发出,那么这样一直触发,所以我们最好设置为边沿触发(数据来了就读一次)。
但是设置边沿触发时,需要一次读干净,所以循环读。
因为cfd是阻塞的,所以设置cfd为非阻塞。
当read的返回值为-1,并且errno的值被设置为EAGAIN时,代表缓冲区被读取干净
#include "wrap.h"
#include
#include
int main(int argc, char *argv[])
{
// 创建套接字
// 绑定
int lfd = tcp4bind(7777,NULL);
// 监听
listen(lfd,128);
// 创建树
int epfd = epoll_create(1);
// 将lfd上树
struct epoll_event ev,evs[1024];
ev.events=EPOLLIN;//读事件
ev.data.fd = lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
// 循环监听树
while(1)
{
int n = epoll_wait(epfd,evs,1024,-1);
printf("epoll_wait\n");
if(n < 0)
{
perr_exit("epoll");
}
else if(n >=0 )
{
for(int i=0;i<n;i++)
{
int fd = evs[i].data.fd;
//如果是lfd变化,并且是读事件变化
if(fd == lfd && evs[i].events&EPOLLIN)
{
struct sockaddr_in cliaddr;
socklen_t len=sizeof(cliaddr);
char ip[16]="";
int cfd = Accept(lfd,(struct sockaddr*)&cliaddr,&len);
printf("client ip=%s port=%d\n",
inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16),
ntohs(cliaddr.sin_port));
//cfd设置为非阻塞
int flag = fcntl(cfd,F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd,F_SETFL,flag);
//将cfd上树
ev.data.fd = cfd;
ev.events = EPOLLIN |EPOLLET; //设置成边沿触发
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
}
else if( evs[i].events&EPOLLIN)//是cfd变化 并且是读事件
{
while(1)
{
char buf[5]="";
int count = Read(fd,buf,sizeof(buf));
if(count < 0)
{
if(errno == EAGAIN)//缓冲区无数据,读干净了
{
break;
}
else//出错
{
printf("error \n");
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&evs[i]);
break;
}
}
if(count ==0 )
{
printf("client close\n");
close(fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&evs[i]);
break;
}
else
{
//printf("%s\n",buf);
write(1,buf,count);
Write(fd,buf,count);
}
}
}
}
}
} // 处理
return 0;
}
视频介绍了简单版、复杂版
#ifndef _THREADPOOL_H
#define _THREADPOOL_H
#include
#include
#include
#include
#include
#include
#include "sys/epoll.h"
#include "wrap.h"
typedef struct _PoolTask
{
int tasknum;//模拟任务编号
void *arg;//回调函数参数
void (*task_func)(void *arg);//任务的回调函数
int fd;
int epfd;
struct epoll_event *e;
}PoolTask ;
typedef struct _ThreadPool
{
int max_job_num;//最大任务个数
int job_num;//实际任务个数
PoolTask *tasks;//任务队列数组
int job_push;//入队位置
int job_pop;// 出队位置
int thr_num;//线程池内线程个数
pthread_t *threads;//线程池内线程数组
int shutdown;//是否关闭线程池
pthread_mutex_t pool_lock;//线程池的锁
pthread_cond_t empty_task;//任务队列为空的条件
pthread_cond_t not_empty_task;//任务队列不为空的条件
}ThreadPool;
void create_threadpool(int thrnum,int maxtasknum);//创建线程池--thrnum 代表线程个数,maxtasknum 最大任务个数
void destroy_threadpool(ThreadPool *pool);//摧毁线程池
//void addtask(ThreadPool *pool);//添加任务到线程池
void addtask(ThreadPool *pool,int fd,struct epoll_event *evs);//
void taskRun(void *arg);//任务回调函数
#endif
//简易版线程池
#include "threadpoolsimple.h"
#include
#include
ThreadPool *thrPool = NULL;
int beginnum = 1000;
void *thrRun(void *arg)
{
//printf("begin call %s-----\n",__FUNCTION__);
ThreadPool *pool = (ThreadPool*)arg;
int taskpos = 0;//任务位置
PoolTask *task = (PoolTask *)malloc(sizeof(PoolTask));
while(1)
{
//获取任务,先要尝试加锁
pthread_mutex_lock(&thrPool->pool_lock);
//无任务并且线程池不是要摧毁
while(thrPool->job_num <= 0 && !thrPool->shutdown )
{
//如果没有任务,线程会阻塞
pthread_cond_wait(&thrPool->not_empty_task,&thrPool->pool_lock);
}
if(thrPool->job_num)
{
//有任务需要处理
taskpos = (thrPool->job_pop++)%thrPool->max_job_num;
//printf("task out %d...tasknum===%d tid=%lu\n",taskpos,thrPool->tasks[taskpos].tasknum,pthread_self());
//为什么要拷贝?避免任务被修改,生产者会添加任务
memcpy(task,&thrPool->tasks[taskpos],sizeof(PoolTask));
task->arg = task;
thrPool->job_num--;
//task = &thrPool->tasks[taskpos];
pthread_cond_signal(&thrPool->empty_task);//通知生产者
}
if(thrPool->shutdown)
{
//代表要摧毁线程池,此时线程退出即可
//pthread_detach(pthread_self());//临死前分家
pthread_mutex_unlock(&thrPool->pool_lock);
free(task);
pthread_exit(NULL);
}
//释放锁
pthread_mutex_unlock(&thrPool->pool_lock);
task->task_func(task->arg);//执行回调函数
}
//printf("end call %s-----\n",__FUNCTION__);
}
//创建线程池
void create_threadpool(int thrnum,int maxtasknum)
{
printf("begin call %s-----\n",__FUNCTION__);
thrPool = (ThreadPool*)malloc(sizeof(ThreadPool));
thrPool->thr_num = thrnum;
thrPool->max_job_num = maxtasknum;
thrPool->shutdown = 0;//是否摧毁线程池,1代表摧毁
thrPool->job_push = 0;//任务队列添加的位置
thrPool->job_pop = 0;//任务队列出队的位置
thrPool->job_num = 0;//初始化的任务个数为0
thrPool->tasks = (PoolTask*)malloc((sizeof(PoolTask)*maxtasknum));//申请最大的任务队列
//初始化锁和条件变量
pthread_mutex_init(&thrPool->pool_lock,NULL);
pthread_cond_init(&thrPool->empty_task,NULL);
pthread_cond_init(&thrPool->not_empty_task,NULL);
int i = 0;
thrPool->threads = (pthread_t *)malloc(sizeof(pthread_t)*thrnum);//申请n个线程id的空间
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
for(i = 0;i < thrnum;i++)
{
pthread_create(&thrPool->threads[i],&attr,thrRun,(void*)thrPool);//创建多个线程
}
//printf("end call %s-----\n",__FUNCTION__);
}
//摧毁线程池
void destroy_threadpool(ThreadPool *pool)
{
pool->shutdown = 1;//开始自爆
pthread_cond_broadcast(&pool->not_empty_task);//诱杀
int i = 0;
for(i = 0; i < pool->thr_num ; i++)
{
pthread_join(pool->threads[i],NULL);
}
pthread_cond_destroy(&pool->not_empty_task);
pthread_cond_destroy(&pool->empty_task);
pthread_mutex_destroy(&pool->pool_lock);
free(pool->tasks);
free(pool->threads);
free(pool);
}
//添加任务到线程池
//addtask(thrPool,evs[i].data.fd,e);
void addtask(ThreadPool *pool,int fd,struct epoll_event *e,int epfd)
{
printf("begin call %s-----\n",__FUNCTION__);
pthread_mutex_lock(&pool->pool_lock);
//实际任务总数大于最大任务个数则阻塞等待(等待任务被处理)
while(pool->max_job_num <= pool->job_num)
{
pthread_cond_wait(&pool->empty_task,&pool->pool_lock);
}
int taskpos = (pool->job_push++)%pool->max_job_num;
//printf("add task %d tasknum===%d\n",taskpos,beginnum);
//pool->tasks[taskpos].tasknum = beginnum++;
pool->tasks[taskpos].arg = (void*)&pool->tasks[taskpos];
pool->tasks[taskpos].task_func = taskRun;
pool->tasks[taskpos].fd = fd;
pool->tasks[taskpos].epfd =epfd;
pool->tasks[taskpos].e = e;
pool->job_num++;
pthread_mutex_unlock(&pool->pool_lock);
pthread_cond_signal(&pool->not_empty_task);//通知包身工
printf("end call %s-----\n",__FUNCTION__);
}
//任务回调函数
void taskRun(void *arg)
{
PoolTask *task = (PoolTask*)arg;
char buf[1024]="";
while(1)
{
char buf[1024]="";
int n = Read(task->fd , buf,sizeof(buf));
if(n < 0 )
{
if(errno == EAGAIN)//缓冲区如果读干净了,read返回值小于0,errno设置成EAGAIN
break;
close(task->fd );//关闭cfd
//epoll_ctl(epfd,EPOLL_CTL_DEL,task->fd ,&evs[i]);//将cfd上树
epoll_ctl(task->epfd,EPOLL_CTL_DEL,task->fd,task->e);//将cfd
printf("client err\n");
break;
}
else if(n == 0)
{
close(task->fd );//关闭cfd
epoll_ctl(task->epfd,EPOLL_CTL_DEL,task->fd,task->e);//将cfd
printf("client close\n");
break;
}
else
{
printf("%s\n",buf );
Write(task->fd ,buf,n);
}
}
free(task->e);
}
int main()
{
create_threadpool(3,20);
int i = 0;
//创建套接字,绑定
int lfd = tcp4bind(8000,NULL);
//监听
listen(lfd,128);
//创建树
int epfd = epoll_create(1);
struct epoll_event ev,evs[1024];
ev.data.fd = lfd;
ev.events = EPOLLIN;//监听读事件
//将ev上树
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
while(1)
{
int nready = epoll_wait(epfd,evs,1024,-1);
if(nready < 0)
perr_exit("err");
else if(nready == 0)
continue;
else if(nready > 0 )
{
for(int i=0;i<nready;i++)
{
if(evs[i].data.fd == lfd && evs[i].events & EPOLLIN)//如果是lfd变化,并且是读事件
{
struct sockaddr_in cliaddr;
char buf_ip[16]="";
socklen_t len = sizeof(cliaddr);
int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);
printf("client ip=%s port=%d\n",inet_ntop(AF_INET,
&cliaddr.sin_addr.s_addr,buf_ip,sizeof(buf_ip)),
ntohs(cliaddr.sin_port));
ev.data.fd = cfd;//cfd上树
//ev.events = EPOLLIN;//监听读事件
ev.events = EPOLLIN | EPOLLET ;//监听读事件
//设置文件描述符cfd为非阻塞
int flag = fcntl(cfd,F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd,F_SETFL,flag);
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);//将cfd上树
}
else if(evs[i].events & EPOLLIN)//普通的读事件
{
struct epoll_event *e = (struct epoll_event*)malloc(sizeof(struct epoll_event));
memcpy(e,&evs[i],sizeof(struct epoll_event));
addtask(thrPool,evs[i].data.fd,e,epfd);
}
}
}
}
close(lfd);
destroy_threadpool(thrPool);
return 0;
}
复杂版线程池代码
#ifndef __THREADPOOL_H_
#define __THREADPOOL_H_
typedef struct threadpool_t threadpool_t;
/**
* @function threadpool_create
* @descCreates a threadpool_t object.
* @param thr_num thread num
* @param max_thr_num max thread size
* @param queue_max_size size of the queue.
* @return a newly created thread pool or NULL
*/
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);
/**
* @function threadpool_add
* @desc add a new task in the queue of a thread pool
* @param pool Thread pool to which add the task.
* @param function Pointer to the function that will perform the task.
* @param argument Argument to be passed to the function.
* @return 0 if all goes well,else -1
*/
int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg);
/**
* @function threadpool_destroy
* @desc Stops and destroys a thread pool.
* @param pool Thread pool to destroy.
* @return 0 if destory success else -1
*/
int threadpool_destroy(threadpool_t *pool);
/**
* @desc get the thread num
* @pool pool threadpool
* @return # of the thread
*/
int threadpool_all_threadnum(threadpool_t *pool);
/**
* desc get the busy thread num
* @param pool threadpool
* return # of the busy thread
*/
int threadpool_busy_threadnum(threadpool_t *pool);
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include "threadpool.h"
#define DEFAULT_TIME 10 /*10s检测一次*/
#define MIN_WAIT_TASK_NUM 10 /*如果queue_size > MIN_WAIT_TASK_NUM 添加新的线程到线程池*/
#define DEFAULT_THREAD_VARY 10 /*每次创建和销毁线程的个数*/
#define true 1
#define false 0
typedef struct
{
void *(*function)(void *); /* 函数指针,回调函数 */
void *arg; /* 上面函数的参数 */
} threadpool_task_t; /* 各子线程任务结构体 */
/* 描述线程池相关信息 */
struct threadpool_t
{
pthread_mutex_t lock; /* 用于锁住本结构体 */
pthread_mutex_t thread_counter; /* 记录忙状态线程个数de琐 -- busy_thr_num */
pthread_cond_t queue_not_full; /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
pthread_cond_t queue_not_empty; /* 任务队列里不为空时,通知等待任务的线程 */
pthread_t *threads; /* 存放线程池中每个线程的tid。数组 */
pthread_t adjust_tid; /* 存管理线程tid */
threadpool_task_t *task_queue; /* 任务队列(数组首地址) */
int min_thr_num; /* 线程池最小线程数 */
int max_thr_num; /* 线程池最大线程数 */
int live_thr_num; /* 当前存活线程个数 */
int busy_thr_num; /* 忙状态线程个数 */
int wait_exit_thr_num; /* 要销毁的线程个数 */
int queue_front; /* task_queue队头下标 */
int queue_rear; /* task_queue队尾下标 */
int queue_size; /* task_queue队中实际任务数 */
int queue_max_size; /* task_queue队列可容纳任务数上限 */
int shutdown; /* 标志位,线程池使用状态,true或false */
};
void *threadpool_thread(void *threadpool);
void *adjust_thread(void *threadpool);
int is_thread_alive(pthread_t tid);
int threadpool_free(threadpool_t *pool);
//threadpool_create(3,100,100);
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
{
int i;
threadpool_t *pool = NULL;
do
{
if((pool = (threadpool_t *)malloc(sizeof(threadpool_t))) == NULL)
{
printf("malloc threadpool fail");
break; /*跳出do while*/
}
pool->min_thr_num = min_thr_num;
pool->max_thr_num = max_thr_num;
pool->busy_thr_num = 0;
pool->live_thr_num = min_thr_num; /* 活着的线程数 初值=最小线程数 */
pool->wait_exit_thr_num = 0;
pool->queue_size = 0; /* 有0个产品 */
pool->queue_max_size = queue_max_size;
pool->queue_front = 0;
pool->queue_rear = 0;
pool->shutdown = false; /* 不关闭线程池 */
/* 根据最大线程上限数, 给工作线程数组开辟空间, 并清零 */
pool->threads = (pthread_t *)malloc(sizeof(pthread_t)*max_thr_num);
if (pool->threads == NULL)
{
printf("malloc threads fail");
break;
}
memset(pool->threads, 0, sizeof(pthread_t)*max_thr_num);
/* 队列开辟空间 */
pool->task_queue = (threadpool_task_t *)malloc(sizeof(threadpool_task_t)*queue_max_size);
if (pool->task_queue == NULL)
{
printf("malloc task_queue fail\n");
break;
}
/* 初始化互斥琐、条件变量 */
if (pthread_mutex_init(&(pool->lock), NULL) != 0
|| pthread_mutex_init(&(pool->thread_counter), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_empty), NULL) != 0
|| pthread_cond_init(&(pool->queue_not_full), NULL) != 0)
{
printf("init the lock or cond fail\n");
break;
}
//启动工作线程
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
for (i = 0; i < min_thr_num; i++)
{
pthread_create(&(pool->threads[i]), &attr, threadpool_thread, (void *)pool);/*pool指向当前线程池*/
printf("start thread 0x%x...\n", (unsigned int)pool->threads[i]);
}
//创建管理者线程
pthread_create(&(pool->adjust_tid), &attr, adjust_thread, (void *)pool);
return pool;
} while (0);
/* 前面代码调用失败时,释放poll存储空间 */
threadpool_free(pool);
return NULL;
}
/* 向线程池中 添加一个任务 */
//threadpool_add(thp, process, (void*)&num[i]); /* 向线程池中添加任务 process: 小写---->大写*/
int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg)
{
pthread_mutex_lock(&(pool->lock));
/* ==为真,队列已经满, 调wait阻塞 */
while ((pool->queue_size == pool->queue_max_size) && (!pool->shutdown))
{
pthread_cond_wait(&(pool->queue_not_full), &(pool->lock));
}
if (pool->shutdown)
{
pthread_cond_broadcast(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
return 0;
}
/* 清空 工作线程 调用的回调函数 的参数arg */
if (pool->task_queue[pool->queue_rear].arg != NULL)
{
pool->task_queue[pool->queue_rear].arg = NULL;
}
/*添加任务到任务队列里*/
pool->task_queue[pool->queue_rear].function = function;
pool->task_queue[pool->queue_rear].arg = arg;
pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size; /* 队尾指针移动, 模拟环形 */
pool->queue_size++;
/*添加完任务后,队列不为空,唤醒线程池中 等待处理任务的线程*/
pthread_cond_signal(&(pool->queue_not_empty));
pthread_mutex_unlock(&(pool->lock));
return 0;
}
/* 线程池中各个工作线程 */
void *threadpool_thread(void *threadpool)
{
threadpool_t *pool = (threadpool_t *)threadpool;
threadpool_task_t task;
while (true)
{
/* Lock must be taken to wait on conditional variable */
/*刚创建出线程,等待任务队列里有任务,否则阻塞等待任务队列里有任务后再唤醒接收任务*/
pthread_mutex_lock(&(pool->lock));
/*queue_size == 0 说明没有任务,调 wait 阻塞在条件变量上, 若有任务,跳过该while*/
while ((pool->queue_size == 0) && (!pool->shutdown))
{
printf("thread 0x%x is waiting\n", (unsigned int)pthread_self());
pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));//暂停到这
/*清除指定数目的空闲线程,如果要结束的线程个数大于0,结束线程*/
if (pool->wait_exit_thr_num > 0)
{
pool->wait_exit_thr_num--;
/*如果线程池里线程个数大于最小值时可以结束当前线程*/
if (pool->live_thr_num > pool->min_thr_num)
{
printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
pool->live_thr_num--;
pthread_mutex_unlock(&(pool->lock));
//pthread_detach(pthread_self());
pthread_exit(NULL);
}
}
}
/*如果指定了true,要关闭线程池里的每个线程,自行退出处理---销毁线程池*/
if (pool->shutdown)
{
pthread_mutex_unlock(&(pool->lock));
printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
//pthread_detach(pthread_self());
pthread_exit(NULL); /* 线程自行结束 */
}
/*从任务队列里获取任务, 是一个出队操作*/
task.function = pool->task_queue[pool->queue_front].function;
task.arg = pool->task_queue[pool->queue_front].arg;
pool->queue_front = (pool->queue_front + 1) % pool->queue_max_size; /* 出队,模拟环形队列 */
pool->queue_size--;
/*通知可以有新的任务添加进来*/
pthread_cond_broadcast(&(pool->queue_not_full));
/*任务取出后,立即将 线程池琐 释放*/
pthread_mutex_unlock(&(pool->lock));
/*执行任务*/
printf("thread 0x%x start working\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter)); /*忙状态线程数变量琐*/
pool->busy_thr_num++; /*忙状态线程数+1*/
pthread_mutex_unlock(&(pool->thread_counter));
(*(task.function))(task.arg); /*执行回调函数任务*/
//task.function(task.arg); /*执行回调函数任务*/
/*任务结束处理*/
printf("thread 0x%x end working\n", (unsigned int)pthread_self());
pthread_mutex_lock(&(pool->thread_counter));
pool->busy_thr_num--; /*处理掉一个任务,忙状态数线程数-1*/
pthread_mutex_unlock(&(pool->thread_counter));
}
pthread_exit(NULL);
}
/* 管理线程 */
void *adjust_thread(void *threadpool)
{
int i;
threadpool_t *pool = (threadpool_t *)threadpool;
while (!pool->shutdown)
{
sleep(DEFAULT_TIME); /*定时 对线程池管理*/
pthread_mutex_lock(&(pool->lock));
int queue_size = pool->queue_size; /* 关注 任务数 */
int live_thr_num = pool->live_thr_num; /* 存活 线程数 */
pthread_mutex_unlock(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
int busy_thr_num = pool->busy_thr_num; /* 忙着的线程数 */
pthread_mutex_unlock(&(pool->thread_counter));
/* 创建新线程 算法: 任务数大于最小线程池个数, 且存活的线程数少于最大线程个数时 如:30>=10 && 40<100*/
if (queue_size >= MIN_WAIT_TASK_NUM && live_thr_num < pool->max_thr_num)
{
pthread_mutex_lock(&(pool->lock));
int add = 0;
/*一次增加 DEFAULT_THREAD 个线程*/
for (i = 0; i < pool->max_thr_num && add < DEFAULT_THREAD_VARY
&& pool->live_thr_num < pool->max_thr_num; i++)
{
if (pool->threads[i] == 0 || !is_thread_alive(pool->threads[i]))
{
pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool);
add++;
pool->live_thr_num++;
}
}
pthread_mutex_unlock(&(pool->lock));
}
/* 销毁多余的空闲线程 算法:忙线程X2 小于 存活的线程数 且 存活的线程数 大于 最小线程数时*/
if ((busy_thr_num * 2) < live_thr_num && live_thr_num > pool->min_thr_num)
{
/* 一次销毁DEFAULT_THREAD个线程, 隨機10個即可 */
pthread_mutex_lock(&(pool->lock));
pool->wait_exit_thr_num = DEFAULT_THREAD_VARY; /* 要销毁的线程数 设置为10 */
pthread_mutex_unlock(&(pool->lock));
for (i = 0; i < DEFAULT_THREAD_VARY; i++)
{
/* 通知处在空闲状态的线程, 他们会自行终止*/
pthread_cond_signal(&(pool->queue_not_empty));
}
}
}
return NULL;
}
int threadpool_destroy(threadpool_t *pool)
{
int i;
if (pool == NULL)
{
return -1;
}
pool->shutdown = true;
/*先销毁管理线程*/
//pthread_join(pool->adjust_tid, NULL);
for (i = 0; i < pool->live_thr_num; i++)
{
/*通知所有的空闲线程*/
pthread_cond_broadcast(&(pool->queue_not_empty));
}
/*for (i = 0; i < pool->live_thr_num; i++)
{
pthread_join(pool->threads[i], NULL);
}*/
threadpool_free(pool);
return 0;
}
int threadpool_free(threadpool_t *pool)
{
if (pool == NULL)
{
return -1;
}
if (pool->task_queue)
{
free(pool->task_queue);
}
if (pool->threads)
{
free(pool->threads);
pthread_mutex_lock(&(pool->lock));
pthread_mutex_destroy(&(pool->lock));
pthread_mutex_lock(&(pool->thread_counter));
pthread_mutex_destroy(&(pool->thread_counter));
pthread_cond_destroy(&(pool->queue_not_empty));
pthread_cond_destroy(&(pool->queue_not_full));
}
free(pool);
pool = NULL;
return 0;
}
int threadpool_all_threadnum(threadpool_t *pool)
{
int all_threadnum = -1;
pthread_mutex_lock(&(pool->lock));
all_threadnum = pool->live_thr_num;
pthread_mutex_unlock(&(pool->lock));
return all_threadnum;
}
int threadpool_busy_threadnum(threadpool_t *pool)
{
int busy_threadnum = -1;
pthread_mutex_lock(&(pool->thread_counter));
busy_threadnum = pool->busy_thr_num;
pthread_mutex_unlock(&(pool->thread_counter));
return busy_threadnum;
}
int is_thread_alive(pthread_t tid)
{
int kill_rc = pthread_kill(tid, 0); //发0号信号,测试线程是否存活
if (kill_rc == ESRCH)
{
return false;
}
return true;
}
/*测试*/
#if 1
/* 线程池中的线程,模拟处理业务 */
void *process(void *arg)
{
printf("thread 0x%x working on task %d\n ",(unsigned int)pthread_self(),*(int *)arg);
sleep(1);
printf("task %d is end\n", *(int *)arg);
return NULL;
}
int main(void)
{
/*threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);*/
threadpool_t *thp = threadpool_create(3,100,100); /*创建线程池,池里最小3个线程,最大100,队列最大100*/
printf("pool inited");
//int *num = (int *)malloc(sizeof(int)*20);
int num[20], i;
for (i = 0; i < 20; i++)
{
num[i]=i;
printf("add task %d\n",i);
threadpool_add(thp, process, (void*)&num[i]); /* 向线程池中添加任务 */
}
sleep(10); /* 等子线程完成任务 */
threadpool_destroy(thp);
return 0;
}
#endif
文件描述符 事件 处理任务
反应堆借鉴了c++思想,将文件描述符 事件 回调封装在一起
libevent事件库里面的代码(后面会详细讲这个事件库)
//反应堆简单版
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "wrap.h"
#define _BUF_LEN_ 1024
#define _EVENT_SIZE_ 1024
//全局epoll树的根
int gepfd = 0;
//事件驱动结构体
typedef struct xx_event{
int fd;
int events;
void (*call_back)(int fd,int events,void *arg);
void *arg;
char buf[1024];
int buflen;
int epfd;
}xevent;
xevent myevents[_EVENT_SIZE_+1];
void readData(int fd,int events,void *arg);
//添加事件
//eventadd(lfd,EPOLLIN,initAccept,&myevents[_EVENT_SIZE_-1],&myevents[_EVENT_SIZE_-1]);
void eventadd(int fd,int events,void (*call_back)(int ,int ,void *),void *arg,xevent *ev)
{
ev->fd = fd;
ev->events = events;
//ev->arg = arg;//代表结构体自己,可以通过arg得到结构体的所有信息
ev->call_back = call_back;
struct epoll_event epv;
epv.events = events;
epv.data.ptr = ev;//核心思想
epoll_ctl(gepfd,EPOLL_CTL_ADD,fd,&epv);//上树
}
//修改事件
//eventset(fd,EPOLLOUT,senddata,arg,ev);
void eventset(int fd,int events,void (*call_back)(int ,int ,void *),void *arg,xevent *ev)
{
ev->fd = fd;
ev->events = events;
//ev->arg = arg;
ev->call_back = call_back;
struct epoll_event epv;
epv.events = events;
epv.data.ptr = ev;
epoll_ctl(gepfd,EPOLL_CTL_MOD,fd,&epv);//修改
}
//删除事件
void eventdel(xevent *ev,int fd,int events)
{
printf("begin call %s\n",__FUNCTION__);
ev->fd = 0;
ev->events = 0;
ev->call_back = NULL;
memset(ev->buf,0x00,sizeof(ev->buf));
ev->buflen = 0;
struct epoll_event epv;
epv.data.ptr = NULL;
epv.events = events;
epoll_ctl(gepfd,EPOLL_CTL_DEL,fd,&epv);//下树
}
//发送数据
void senddata(int fd,int events,void *arg)
{
printf("begin call %s\n",__FUNCTION__);
xevent *ev = arg;
Write(fd,ev->buf,ev->buflen);
eventset(fd,EPOLLIN,readData,arg,ev);
}
//读数据
void readData(int fd,int events,void *arg)
{
printf("begin call %s\n",__FUNCTION__);
xevent *ev = arg;
ev->buflen = Read(fd,ev->buf,sizeof(ev->buf));
if(ev->buflen>0) //读到数据
{
//void eventset(int fd,int events,void (*call_back)(int ,int ,void *),void *arg,xevent *ev)
eventset(fd,EPOLLOUT,senddata,arg,ev);
}
else if(ev->buflen==0) //对方关闭连接
{
Close(fd);
eventdel(ev,fd,EPOLLIN);
}
}
//新连接处理
void initAccept(int fd,int events,void *arg)
{
printf("begin call %s,gepfd =%d\n",__FUNCTION__,gepfd);//__FUNCTION__ 函数名
int i;
struct sockaddr_in addr;
socklen_t len = sizeof(addr);
int cfd = Accept(fd,(struct sockaddr*)&addr,&len);//是否会阻塞? 不会
//查找myevents数组中可用的位置
for(i = 0 ; i < _EVENT_SIZE_; i ++)
{
if(myevents[i].fd==0)
{
break;
}
}
//设置读事件
eventadd(cfd,EPOLLIN,readData,&myevents[i],&myevents[i]);
}
int main(int argc,char *argv[])
{
//创建socket
int lfd = Socket(AF_INET,SOCK_STREAM,0);
//端口复用
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
//绑定
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8888);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(lfd,(struct sockaddr*)&servaddr,sizeof(servaddr));
//监听
Listen(lfd,128);
//创建epoll树根节点
gepfd = epoll_create(1024);
printf("gepfd === %d\n",gepfd);
struct epoll_event events[1024];
//添加最初始事件,将侦听的描述符上树
eventadd(lfd,EPOLLIN,initAccept,&myevents[_EVENT_SIZE_],&myevents[_EVENT_SIZE_]);
//void eventadd(int fd,int events,void (*call_back)(int ,int ,void *),void *arg,xevent *ev)
while(1)
{
int nready = epoll_wait(gepfd,events,1024,-1);
if(nready<0) //调用epoll_wait失败
{
perr_exit("epoll_wait error");
}
else if(nready>0) //调用epoll_wait成功,返回有事件发生的文件描述符的个数
{
int i = 0;
for(i=0;i<nready; i++)
{
xevent *xe = events[i].data.ptr;//取ptr指向结构体地址
printf("fd=%d\n",xe->fd);
if(xe->events & events[i].events)
{
xe->call_back(xe->fd,xe->events,xe);//调用事件对应的回调
}
}
}
}
//关闭监听文件描述符
Close(lfd);
return 0;
}
Web服务器又称WWW服务器、网站服务器等
特点
使用HTTP协议与客户机浏览器进行信息交流
不仅能存储信息,还能在用户通过web浏览器提供的信息的基础上运行脚本和程序
该服务器需可安装在UNIX、Linux或Windows等操作系统上
著名的服务器有Apache、Tomcat、 IIS等
Webserver—HTTP协议
概念
一种详细规定了浏览器和万维网服务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议
特点
1、支持C/S架构
2、简单快速:客户向服务器请求服务时,只需传送请求方法和路径 ,常用方法:GET、POST
3、无连接:限制每次连接只处理一个请求
4、无状态:即如果后续处理需要前面的信息,它必须重传,这样可能导致每次连接传送的数据量会增大
网页浏览(使用GET方式)
客户端浏览器请求:
服务器收到的数据:
服务器应答的格式:请求失败
通过对TCP、UDP的编程学习,能够完成对实际项目需求中网络功能的开发,为了提高程序的稳定性以及效率等等,通常会使用多线程、多进程开发;根据功能需求的不同,可以利用C/S、B/S模式进行开发
作为嵌入式工程师,需要对整个网络通信的过程进行掌握,从一个整体的角度来开发出更加稳定、效率的网络程序
SWITCH:交换机
router:路由器
网络通信过程分析软件
Packet Tracer 是由Cisco公司发布的一个辅助学习工具,提供了设计、配置、排除网络故障网络模拟环境
可以直接使用拖曳方法建立网络拓扑,并可提供数据包在网络中行进的详细处理过程,观察网络实时运行情况
网络交换机(又称“网络交换器”),是一个扩大网络的器材,可以把更多的计算机等网络设备连接到当前的网络中。
具有性价比高、高度灵活、相对简单、易于实现等特点
以太网技术已成为当今最重要的一种局域网组网技术,网络交换机也就成为了最普及的交换机
企业级(好几千一个),常见品牌:华为、TP-LINK,思科、腾达
1、转发过滤:当一个数据帧的目的地址在MAC地址表中有映射时,它被转发到连接目的节点的端口而不是所有端口(如该数据帧为广播/组播帧则转发至所有端口)
2、学习功能:以太网交换机了解每一端口相连设备的MAC地址,并将地址同相应的端口映射起来存放在交换机缓存中的MAC地址表中
3、目前交换机还具备了一些新的功能,如对VLAN(虚拟局域网)的支持、对链路汇聚的支持,甚至有的还具有防火墙的功能
左边是路由表
举例
每台PC必须手动设置ip、netmask
192.168.1.1/255.255.255.0
……
192.168.1.8/255.255.255.0
参考演示demo-1
总结:
如果PC不知目标IP所对应的MAC,那么可以看出,PC会先发送ARP广播,得到对方的MAC然后,再进行数据的传送
当switch第一次收到ARP广播数据,会把ARP广播数据包转发给所有端口(除来源端口);如果以后还有PC询问此IP的MAC,那么只是向目标的端口进行转发数据(如果是HUB集线器,则每次都广播)
每台PC都会有一个ARP缓存表,用来记录IP所对应的的MAC
每个路由器至少有两个网卡,两边分别与局域网连接(区分网段的,网关,其IP地址最后一位设置为1或者254)
路由器(Router)又称网关设备(Gateway)是用于连接多个逻辑上分开的网络
所谓逻辑网络是代表一个单独的网络或者一个子网。当数据从一个子网传输到另一个子网时,可通过路由器的路由功能来完成
具有判断网络地址和选择IP路径的功能
家用级
举例
配置所有PC的IP、netmask
PC0(192.168.1.1/24)
PC1(192.168.1.2/24)
PC2(192.168.3.1/24)
PC3(192.168.3.2/24)
配置router(每个router有2个IP,两个网卡)
router0(192.168.1.254/24,192.168.2.1/24)
router1(192.168.3.254/24,192.168.2.2/24)
配置后的网络信息如下
参考演示demo-2(PC0 ping PC3)
总结:
不在同一网段的PC,需要设置默认网关才能把数据传送过去
通常情况下,都会把路由器设为默认网关
当路由器收到一个其它网段的数据包时,会根据“路由表”来决定把此数据包发送到哪个端口;路由表的设定有静态和动态方法
在dos控制端下,可通过输入命令查看路由表
网络通信过程(复杂)
以PC0访问www.helloworld.com举例
配置网络设备
PC:IP、NETMASK、DFGATEWAY、DNS
ROUTER:IP、NETMASK、路由表
总结
1、DNS服务器的作用是解析出IP
2、DFGATEWAY指定发往其它网段的数据包转发的路径
3、在路由器中路由表指定数据包的“下一跳”的地址
4、公有IP、私有IP
防火墙的定义
防火墙被定义成一个或一组设备,它在网络之间执行访问控制策略
防火墙的分类
硬件防火墙、软件防火墙
防火墙最重要的任务
1、切割被信任(如子域)与不被信任(如 Internet)的网段
2、划分出可提供Internet的服务与必须受保护的服务
3、分析出可接受与不可接受的数据包状态
你需不需要防火墙?
理论上需要,但你必须知道系统哪些数据与服务需要保护、针对需要受保护的服务来设置防火墙规则
单一网络,仅有一个路由器
只要管理这一台防火墙主机就可以很轻易的将来自Internet的不良网络数据包阻挡掉
如果入侵从LAN进入,那咋办?
内部网络包含安全性更高的子网,需要内部防火墙切开子网
架设在防火墙后端的主机服务器
设置了防火墙也不能保证网络就一定安全
防火墙不能有效阻止病毒或木马程序
防火墙对于来自内部LAN的攻击无能为力
iptables根据数据包的分析资料“对比”预先定义的规则内容
对比结果符合Rule1,此时这个网络数据包就会进行Action1的动作,而不会理会后续的Rule2、Rule3等规则了
iptables有多个表格(table)。而每个表格又有多个链(chain)
1、Filter(过滤器):与本机数据有关
INPUT:主要与想要进入Linux本机的数据包有关
OUTPUT:主要与Linux本机所要送出的数据包有关
FORWARD:与本机无关,传送数据到后端的计算机中
2、NAT(地址转换):主要用来进行来源和目的地的ip或port的转换
PREROUTING:在进行路由判断之前所要进行的规则
POSTROUTING:在进行路由判断之后所要进行的规则
OUTPUT:与发出去的数据包有关
3、Mangle(破坏者):主要与特殊的数据包的路由标志有关(很少使用)
1.规则的查看
iptables [-t tables] [-L] [-nv]
-t:后面接table,例如nat或filter,若省略则使用filter
-L:列出目前的table的规则
-n:不进行IP与HOSTNAME的反查,这样显示速度快
-v:列出更多的信息(数据包的位数、相关的网络接口)
iptables-save会列出完整的防火墙规则(推荐)
2.规则的清除
iptables [-t tables] [-FXZ]
-F:清除所有已定制的规则
-X:除掉所有用户"自定义"的chain
-Z:将所有的chain的计数与流量统计都归零
例:清除本机防火墙(filter)的所有规则
3.定义默认策略(policy)
iptables [-t nat] -p [INPUT,OUTPUT,FORWARD] [ACCEPT,DROP]
-P:定义策略(Policy),P为大写
ACCEPT:该数据包可接受
DROP:该数据包直接丢弃,不会让client知道为何丢弃
例:将本机的INPUT设置为DROP,其他设置为ACCEPT注意先清除所有规则
例1:设置lo成为受信任的设备,亦即进出lo的数据包都予以接受
例2:只要来自内网的(172.20.223.0/24)的数据包都接受
例3:只要是来自172.20.223.32就接受,但是来自172.20.223.91的数据包就丢弃
例1:想连接到本机的udp port 137,138 tcp port 139,445就放行
例1:只要已建立或相关封包就予以通过,只要是不合法封包就丢弃
例2:针对局域网络内的 aa:bb:cc:dd:ee:ff 主机放行
超简单的客户端防火墙(实例)
1、规则归零:清除所有已经存在的规则
2、默认策略:除了 INPUT 这个自定义链设为 DROP 外,其他为预设 ACCEPT
3、信任本机:由于 lo 对本机来说是相当重要的,因此 lo 必须设定为信任装置
4、回应数据包:让本机主动向外要求而响应的封包可以进入本机 (ESTABLISHED,RELATED)
5、信任用户:这是非必要的,如果你想要让区网的来源可用你的主机资源时
数据报式套接字(SOCK_DGRAM)
1、无连接的socket,针对无连接的 UDP 服务
2、可通过邮件模型来进行对比
流式套接字(SOCK_STREAM)
1、面向连接的socket,针对面向连接的 TCP 服务
2、可通过电话模型来进行对比
这两类套接字似乎涵盖了TCP/IP 应用的全部
TCP与UDP各自有独立的port互不影响
一个进程可同时拥有多个port
不必关心tcp/ip协议实现的过程
client
1、创建socket接口
2、定义sockaddr_in变量,其中ip、port为目的主机的信息
3、可发送0长度的数据包
server
1、bind本地主机的ip、port等信息
2、接收到的数据包中包含来源主机的ip、port信息
client
connect来建立连接
write、read收发数据
不可发送0长度的数据
server
bind本地主机的ip、port等信息
listen把主动套接字变为被动
accept会有新的返回值
多进程、线程完成并发
想一想?
1、能否截获网络中的数据?
2、怎样发送一个自定义的IP包?
3、怎样伪装本地的IP、MAC?
4、网络攻击是怎么回事?
5、路由器、交换机怎样实现?
方向:
TCP/IP协议栈
原始套接字
网络开发工具包libpcap/libnet
书籍:
《TCP/IP详解 卷一》★
《UNIX网络编程 卷一》第三版★
原始套接字(SOCK_RAW)
1、一种不同于SOCK_STREAM、SOCK_DGRAM的套接字,它实现于系统核心
2、可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用
3、开发人员可发送自己组装的数据包到网络上
4、广泛应用于高级网络编程
5、网络专家、黑客通常会用此来编写奇特的网络程序
流式套接字只能收发
TCP协议的数据
数据报套接字只能收发
UDP协议的数据
原始套接字可以收发
1、内核没有处理的数据包,因此要访问其他协议
2、发送的数据需要使用,原始套接字(SOCK_RAW)
int socket(PF_PACKET, SOCK_RAW, protocol)
功能:
创建链路层的原始套接字
参数:
protocol:指定可以接收或发送的数据包类型
ETH_P_IP:IPV4数据包
ETH_P_ARP:ARP数据包
ETH_P_ALL:任何协议类型的数据包
返回值:
成功(>0):链路层套接字
失败(<0):出错
sock_raw_fd = socket(PF_PACKET,SOCK_RAW,htons(ETH_P_ALL));
已过时,不再使用
sock_raw_fd = socket(AF_INET, SOCK_PACKET, htons(ETH_P_ALL));
头文件:
#include
#include
使用原始套接字进行编程开发时,首先要对不同协议的数据包进行学习,需要手动对IP、TCP、UDP、ICMP等包头进行组装或者拆解
ubuntu12.04中描述网络协议结构的文件如下:
在TCP/IP协议栈中的每一层为了能够正确解析出上层的数据包,从而使用一些“协议类型”来标记,详细如下图
ICMP回显请求和回显应答格式
注意:
不同的类型值以及代码值,代表不同的功能
在很多时候需要对网络上的数据进行抓取,然后进行分析,此“网络数据分析器”就是模仿现实开发中的抓包工具而进行的
运行demo现象如下:
说明:
1、ARP的TYPE为0x0806
2、buf为unsinged char
3、所有数据均为大端
说明:
1、IP的TYPE为0x0800
2、buf为unsinged char
3、所有数据均为大端
如下图所示,是网上的数据包的组包过程;其解包过程正好相反,首先分析以太网得到MAC然后再依次分析,比如IP、PORT等等
要求:
1、分析出ARP/IP/RARP
2、分析出MAC
扩展:
在完成基本要求的前提下,分析PORT
提示:
以root权限运行
想一想:
如何捕捉途经网卡的数据?
06-01-data-analysis.c
混杂模式
1、指一台机器的网卡能够接收所有经过它的数据包,而不论其目的地址是否是它。
2、一般计算机网卡都工作在非混杂模式下,如果设置网卡为混杂模式需要root权限
linux下设置
1、设置混杂模式:ifconfig eth0 promisc
2、取消混杂模式:ifconfig eth0 -promisc
windos下通过特定的软件实现
sendto(sock_raw_fd, msg, msg_len, 0,(struct sockaddr*)&sll, sizeof(sll));
注意:
1、sock_raw_fd:原始套接字
2、msg:发送的消息(封装好的协议数据)
3、sll:本机网络接口,指发送的数据应该从本机的哪个网卡出去,而不是以前的目的地址
想一想:
如何定义sll?
struct sockaddr_ll sll;
#include
struct sockaddr_ll
只需要对sll.sll_ifindex赋值,就可使用
int ioctl(int fd, int request,void *)
#include
ioctl获取接口示例
想一想:
ioctl的参数、struct ifreq结构类型
struct ifreq:#include
IFNAMSIZ 16
sendto发送数据的整体过程
想一想:
如果A(192.168.1.1)向B(192.168.1.2)发送一个数据包,那么需要的条件有ip、port、使用的协议(TCP/UDP)之外还需要MAC地址,因为在以太网数据包中MAC地址是必须要有的;问怎样才能知道对方的MAC地址?使用什么协议呢?
ARP(Address Resolution Protocol,地址解析协议)
1、是TCP/IP协议族中的一个
2、主要用于查询指定ip所对应的的MAC
3、请求方使用广播来发送请求
4、应答方使用单播来回送数据
5、为了在发送数据的时候提高效率在计算中会有一个ARP缓存表,用来暂时存放ip所对应的MAC,在linux中使用ARP即可查看,在xp中使用ARP -a
以机器A获取机器B的MAC为例
要求
获取到当前网段中所有机器的MAC地址
提示
1、每次指定一个机器发送MAC请求,通过发送多次ARP,即可得到所有的机器的MAC
2、ARP的发送和接收各使用一个线程
MAC地址扫描器运行现象如下图所示 06-02-sock_raw_arp.c
飞鸽格式
版本:用户名:主机名:命令字:附加消息
目的:挑拨离间
组包过程:
注意:
msg指的是udp报文头中的数据
MAC、IP、UDP报文头参考前面的数据包详解
1、在对UDP校验的时候需要在UDP报文之间加上伪头部
2、IP校验的时候不需要伪头部
06-03-sock_raw_udp_fq.c
TCP数据包(发送一个SYN数据包)
06-04-sock_raw_tcp_syn.c
原始套接字
1、开发者可发送任意的数据包到网上
2、开发网络攻击等特殊软件
3、需要开发者手动组织数据、各种协议包头、校验和计算等等
创建方法:
1、是一个网络数据捕获开发包
2、平台独立具有强大功能
3、是一套高层的编程接口的集合;其隐藏了操作系统的细节,可以捕获网上的所有,包括到达其他主机的数据包
4、使用非常广泛,几乎只要涉及到网络数据包的捕获的功能,都可以用它开发,如wireshark
5、开发语言为C语言,但是也有基于其它语言的开发包,如Python语言的开发包pycap
Libpcap主要的作用如下:
1、捕获各种数据包
列如:网络流量统计
2、过滤网络数据包
列如:过滤掉本地上的一些数据,类似防火墙
3、分析网络数据包
列如:分析网络协议,数据的采集
4、存储网络数据包
列如:保存捕获的数据以为将来进行分析
apt-get方式:
利用libpcap函数库开发应用程序的基本步骤:
1、打开网络设备
2、设置过滤规则
3、捕获数据
4、关闭网络设备
捕获网络数据包常用函数
1、pcap_lookupdev( )
2、pcap_open_live( )
3、pcap_lookupnet( )
4、pcap_compile( )、 pcap_setfilter( )
5、pcap_next( )、pcap_loop( )
6、pcap_close( )
char *pcap_lookupdev(char *errbuf)
功能:
得到可用的网络设备名指针
参数:
errbuf: 存放相关的错误信息
返回值:
成功返回设备名指针;失败返回NULL
例如:
pcap_t *pcap_open_live(const char *device,
int snaplen,
int promisc,
int to_ms,
char *ebuf)
功能:
打开一个用于捕获数据的网络接口
返回值:
返回一个Libpcap句柄
参数:
device:网络接口的名字
snaplen:捕获数据包的长度
promise:1代表混杂模式,其它非混杂模式
to_ms:等待时间
ebuf:存储错误信息
例如
int pcap_lookupnet(char *device,bpf_u_int32 *netp,
bpf_u_int32 *maskp,
char *errbuf)
功能:
获得指定网络设备的网络号和掩码
参数:
device:网络设备名
netp:存放网络号的指针
maskp:存放掩码的指针
errbuf:存放出错信息
返回值:
成功返回0,失败返回-1
例如:
int pcap_compile(pcap_t *p,
struct bpf_program *program,
char *buf,int optimize,
bpf_u_int32 mask)
功能:
编译BPF过滤规则
返回值:
成功返回0,失败返回-1
参数:
p:Libpcap句柄
program:bpf过滤规则
buf:过滤规则字符串
optimize:优化
mask:掩码
例如:
int pcap_setfilter(pcap p,
struct bpf_programfp)
功能:
设置BPF过滤规则
返回值:
成功返回0,失败返回-1
参数:
p:Libpcap句柄
fp:BPF过滤规则
const u_char *pcap_next(pcap_t *p,
struct pcap_pkthdr *h)
功能:
捕获一个网络数据包
参数:
p:Libpcap句柄
h:数据包头
返回值:
捕获的数据包的地址
例如:
int pcap_loop(pcap_t *p,int cnt,
pcap_handler callback,
u_char *user)
功能:
循环捕获网络数据包,直到遇到错误或者满足退出条件;每次捕获一个数据包就会调用callback指示的回调函数,所以,可以在回调函数中进行数据包的处理操作
返回值:
成功返回0,失败返回负数
参数:
p:Libpcap句柄
cnt:指定捕获数据包的个数,如果是-1,就
会永无休止的捕获
callback:回调函数
user:向回调函数中传递的参数
例如:
void pcap_close(pcap_t *p)
功能:
关闭Libpcap操作,并销毁相应的资源
参数
p:需要关闭的Libpcap句柄
返回值:
无
例如
捕获第一个网络数据包(01-Libpcap-demo)
1、gcc编译时需要加上-lpcap
2、运行时需要使用超级权限
捕获指定类型数据包,如ARP(02-Libpcap-demo)
注意:为了更具有说明请多次执行此程序
捕获多个网络数据包(03-Libpcap-demo)
项目要求
判断数据包的类型ARP/RARP/IP/TCP/UDP
分析
MAC(src、dst)
IP(src、dst)
PORT(src、dst)
分析出的UDP/TCP应用程序的通信数据
专业的构造和发送网络数据包的开发工具包
是个高层次的API函数库,允许开发者自己构造和发送网络数据包
Libnet特点
1、隐藏了很多底层细节,省去了很多麻烦;如缓冲区管理、字节流顺序、校验和计算等问题,使开发者把重心放到程序的开发中
2、可以轻松、快捷的构造任何形式的网络数据包,从而开发各种各样的网络程序
3、使用非常广泛,例如著名的软件Ettercap、Firewalk、Snort、Tcpreplay等
4、在1998年就出现了,但那时还有很多缺陷,比如计算校验和非常繁琐等;从2001年开始Libnet的作者Mike
5、Schiffman对其进行了完善,而且功能更加强大。至此,可以说Libnet开发包已经非常完美了,使用Libnet开发包的人越来越多
利用libnet函数库开发应用程序的基本步骤:
1、数据包内存初始化
2、构造数据包
3、发送数据
4、释放资源
libnet库主要功能:
1、内存管理
2、地址解析
3、包处理
内存管理相关函数:
libnet_t *libnet_init(int injection_type, char *device, char *err_buf)
功能:
数据包内存初始化及环境建立
参数:
injection_type: 构造的类型(LIBNET_LINK,LIBNET_RAW4, LIBNET_LINK_ADV,LIBNET_RAW4_ADV)
device:网络接口,如"eth0",或IP地址,亦可为NULL(自动查询搜索)
err_buf: 存放出错的信息
返回值:
成功返回一个libnet句柄;失败返回NULL
内存管理相关函数:
void libnet_destroy(libnet_t *l);
功能:
释放资源
参数:
l: libnet句柄
返回值:
无
例如:
libnet_ptag_t libnet_build_udp(
u_int16_t sp,u_int16_t dp,
u_int16_t len,u_int16_t sum,
u_int8_t *payload,u_int32_t payload_s,
libnet_t *l,libnet_ptag_t ptag)
功能:
构造udp数据包
返回值:
成功返回协议标记;失败返回-1
参数:
sp: 源端口号
dp:目的端口号
len:udp包总长度
sum:校验和,设为0,libnet自动填充
payload:负载,可设置为NULL
payload_s:负载长度,或为0
l: libnet句柄
ptag:协议标记
libnet_ptag_t libnet_build_ipv4(
u_int16_t ip_len,u_int8_t tos,
u_int16_t id,u_int16_t flag,
u_int8_t ttl,u_int8_t prot,
u_int16 sum,u_int32_t src,
u_int32_t dst,u_int8_t *payload,
u_int32_t payload_s,
libnet_t *l,libnet_ptag_t ptag)
功能:
构造一个IPv4数据包
参数:
ip_len:ip包总长
tos:服务类型
id:ip标识
flag:片偏移
ttl:生存时间
prot:上层协议
sum:校验和,设为0,libnet自动填充
src: 源ip地址
dst:目的ip地址
payload:负载,可设置为NULL
payload_s:负载长度,或为0
l: libnet句柄
ptag:协议标记
返回值:
成功返回协议标记;失败返回-1
libnet_ptag_t libnet_build_ethernet(
u_int8_t *dst,u_int8_t *src,
u_int16_t type,
u_int8_t *payload,
u_int32_t payload_s,
libnet_t *l,libnet_ptag_t ptag)
功能:
构造一个以太网数据包
参数:
dst:目的mac
src:源mac
type:上层协议类型
payload:负载,即附带的数据
payload_s:负载长度
l:libnet句柄
ptag:协议标记
返回值:
成功返回协议标记;失败返回-1
int libnet_write(libnet_t * l)
功能:
发送数据到网络
参数:
l:libnet句柄
返回值:
失败返回-1,成功返回其他
更多处理函数请参见附录
如:
libnet_build_tcp( );
libnet_build_tcp_options( );
libnet_build_ipv4_options( );
libnet_build_arp( );
局域网中的网络流通是按照MAC地址进行传输的,可以通过欺骗通信双方的MAC地址,即可达到干扰通信的目的
1、用户可选择要欺骗的ip
2、使用ARP方式来进行欺骗
3、被欺骗方不能正常通信
1、发送ARP请求
2、获得ARP表(并存储)
3、选择欺骗目标ip
4、根据目标ip找到其MAC
5、使用库函数构造ARP回应包
6、发送