网络编程大体内容
IO进程 - 本地进程通信
两台不同主机如何不借助第三方软件通信 - 网络 linux下的网络编程
文件IO 标准IO - 文件(Linux下一切皆是文件,设备(字符设备、块设备))
特殊设备
文章目录
一、网络发展史
【腾讯文档】internet历史
二、局域网和广域网
局域网(LAN)
局域网的缩写是LAN,local area network,顾名思义,是个本地的网络,只能实现小范围短距离的网络通信。我们的家庭网络是典型的局域网。电脑、手机、电视、智能音箱、智能插座都连在路由器上,可以互相通信。局域网,就像是小区里的道路,分支多,连接了很多栋楼。
广域网(Wan)
广域网(Wide Area Network)是相对局域网来讲的,局域网的传输距离比较近,只能是一个小范围的。如果需要长距离的传输,比如某大型企业,总部在北京,分公司在长沙,局域网是无法架设的。广域网,就像是大马路,分支可能少,但类型多,像国道、省道、高速、小道等,连接了很多大的局域网。
2.1 光猫
光猫是一种类似于基带modem(数字调制解调器)的设备,和基带modem不同的是接入的是光纤专线,是光信号。用于广域网中光电信号的转换和接口协议的转换,接入路由器,是广域网接入。
将光线插入左侧的灰色口,右侧网口接网线到路由器即可。
2.2 交换机和路由器
交换机(二层):用于局域网内网的数据转发路由器(三层):用于连接局域网和外网
路由器有交换机的功能,反之不成立,交换机没有IP分配和IP寻址的功能。
交换机各个口是平等的,所有接入的设备需要自己配置IP,然后组成局域网。
路由器需要区分WAN口和LAN口,WAN口是接外网的(从Modem出来的或者从上一级路由器出来的),LAN口是接内网的,现在路由器都带无线功能,本质上无线接入就是LAN。
2.3 网线
三、IP地址
3.1 基本概念
●IP地址是Internet中主机的标识
● Internet中的主机要与别的机器通信必须具有一个IP地址
● IP地址为32位(IPv4)或者128位(IPv6)
● 表示形式:常用点分形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。
ipv4 五类:A B C D E
3.2 ip地址划分(IPv4)
二级划分 ip=网络号+主机号
网络号:表示是否在一个网段内(局域网)
主机号:标识在本网段内的ID,同一局域网不能重复
ip地址取值范围:
A类:1.0.0.1~126.255.255.254
B类:128.0.0.1~~191.255.255.254
C类:192.0.0.1~~223.255.255.254
D类(组播地址):224.0.0.1~~239.255.255.254
E类:保留待用 11110
3.3 特殊地址
0.0.0.0:在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务。
127.0.0.1:回环地址/环路地址,所有发往该类地址的数据包都应该被loop back。
每一个网段主机号为0的地址是网络地址,设置网关主机号为1的地址,主机号最大的地址是该网段的广播地址。全网广播地址255.255.255.255。
3.4 子网掩码
● 子网掩码:是一个32位的整数,作用是将某一个IP划分成网络地址和主机地址;
● 子网掩码长度是和IP地址长度完全一样;
● 网络号全为1,主机号全为0;
255.0.0.0 - A
255.255.0.0 - B
255.255.255.0 - C -
子网掩码 & ip地址 = 网络地址 (网段)
~子网掩码 & ip地址 = 主机地址
3.5 子网号概念
三级地址 ip=网络号+子网号+主机号
网络号+子网号 -网段(网络地址)
练习1:
某公司有四个部门:行政、研发1、研发2、营销,每个部门各30台计算机接入公司局域网交换机,如果要在192.168.1.0网段为每个部门划分子网,子网掩码应该怎么设置,每个子网的地址范围分别是什么?(4个部门之间不能通信) 256 - 4 = 64 => 2^6
192.168.1.0 - 254
子网掩码:255.255.255.1100 0000 - 主机号:0000 0000 - 1111 1111
00 0000 - 11 1111 》64
划分后: 子网掩码-》 255.255.255.192 将主机号划分两位作为网络号:取值-00 01 10 11
192.168.1. 00 网段 000000 - 111111
192.168.1.0 ~ 192.168.1.63
192.168.1.0网络地址 , 192.168.1.63广播地址
192.168.1. 01 网段
192.168.1. 64 ~ 192.168.1.127
192.168.1.64网络地址 , 192.168.1.127广播地址
192.168.1. 10 网段
192.168.1. 128 ~ 192.168.1. 191
192.168.1.128网络地址 , 192.168.1.191广播地址
192.168.1. 11 网段
192.168.1. 192~192.168.1. 255
192.168.1.192网络地址 , 192.168.1.255广播地址
划分为8个网段:每个网段链接30台
192.168.1.0 - 254
子网掩码:255.255.255.1110 0000 - 主机号:0000 0000 - 1111 1111
0 0000 - 1 1111 - >30
000 - 111 000 001 010 011 100 101 110 111
四、网络模型
4.1 网络模型
undefined 网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。
undefined 每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务
undefined 网络体系结构即指网络的层次结构和每层所使用协议的集合
undefined 两类非常重要的体系结构:OSI与TCP/IP
4.2 OSI模型
undefined OSI模型是一个理想化的模型,尚未有完整的实现
undefined OSI模型共有七层
undefined OSI现阶段只用作教学和理论研究
OSI模型
OSI模型是最理想的模型
物理层:传输的是bit流(0与1一样的数据),物理信号,没有格式
链路层:格式变为帧(把数据分成包,一帧一帧的数据进行发送)
网络层:路由器中是有算法的,ip,(主机到主机)(路由的转发)
传输层:端口号,数据传输到具体那个进程程序(端到端)
会话层:通信管理,负责建立或者断开通信连接
表示层:确保一个系统应用层发送的消息可以被另一个系统的应用层读取,编码转换,数据解析,管理数据加密,解密;
应用层:指定特定应用的协议,文件传输,文件管理,电子邮件等。
4.3 TCP/IP模型
TCP/IP参考模型
网络接口和物理层:屏蔽硬件差异(驱动),向上层提供统一的操作接口。
**网络层:**提供端对端的传输,可以理解为通过IP寻址机器。
**传输层:**决定数据交给机器的哪个任务(进程)去处理,通过端口寻址
应用层:应用协议和应用程序的集合
OSI和TCP/IP模型对应关系图
OSI参考模型
4.4 常见网络协议
网络接口和物理层:
ppp:拨号协议(老式电话线上网方式)
ARP:地址解析协议 IP-->MAC
RARP:反向地址转换协议 MAC-->IP
网络层:
IP(IPV4/IPV6):网间互连的协议
ICMP:网络控制管理协议,ping命令使用
IGMP:网络分组管理协议,广播和组播使用
传输层:
TCP:传输控制协议
UDP:用户数据报协议
应用层:
SSH:加密协议
telnet:远程登录协议
FTP:文件传输协议
HTTP:超文本传输协议
DNS:地址解析协议
SMTP/POP3:邮件传输协议
注意:TCP和IP是属于不同协议栈层的,只是这两个协议属于协议族里最重要的协议,所以协议栈或者模型以之命名了。
五、TCP与UDP
UDP TCP 协议相同点:都存在于传输层
TCP(即传输控制协议):
是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、
数据无失序、数据无重复到达的通信)
适用情况:
1、适合于对传输质量要求较高,以及传输大量数据的通信。
2、在需要可靠数据传输的场合,通常使用TCP协议
3、MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
UDP (用户数据报协议):
UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
适用情况:
1、发送小尺寸数据(如对DNS服务器进行IP地址查询时)
2、在接收到数据,给出应答较困难的网络中使用UDP。
3、适合于广播/组播式通信中。
4、MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
5、流媒体、VOD、VoIP、IPTV等网络多媒体服务中通常采用UDP方式进行实时数据传输
六、socket简介
1》1982 - Berkeley Software Distributions 操作系统引入了socket作为本地进程之间通信的接口
2》1986 - Berkeley 扩展了socket 接口,使之支持UNIX 下的TCP/IP 通信
3》现在很多应用 (FTP, Telnet) 都依赖这一接口
Socket
1、是一个编程接口
2、是一种特殊的文件描述符 (everything in Unix is a file)
3、并不仅限于TCP/IP协议
4、面向连接 (Transmission Control Protocol - TCP/IP)
5、无连接 (User Datagram Protocol -UDP 和 Inter-network Packet Exchange - IPX)
为什么需要Socket?
普通的I/O操作过程
•打开文件->读/写操作->关闭文件
•TCP/IP协议被集成到操作系统的内核中,引入了新型的“I/O”操作
•进行网络通信的两个进程在不同的机器上,如何连接?
•网络协议具有多样性,如何进行统一的操作
需要一种通用的网络编程接口:Socket
6.1 socket类型
流式套接字(SOCK_STREAM) TCP
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
数据报套接字(SOCK_DGRAM) UDP
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字(SOCK_RAW)
可以对较低层次协议如IP、ICMP直接访问。
6.2 socket的位置
套接字就是网络进程的ID,可以认为套接字=网络地址ip+端口号。
七、端口号
● 为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区分
● TCP端口号与UDP端口号独立
● 端口号一般由IANA (Internet Assigned Numbers Authority) 管理
● 端口用两个字节来表示 2byte
八、字节序
小端序(little-endian) - 低序字节存储在低地址 (主机字节序)
大端序(big-endian)- 高序字节存储在低地址 (网络字节序)
网络中传输的数据必须使用网络字节序,即大端字节序
笔试题:写一个函数,判断当前主机的字节序?
测试方式:共用体、指针强转、数据类型强转
#include <stdio.h>
union un {
int a;
short b;
char c;
};
int main(int argc, char const *argv[])
{
union un st;
st.a=0x12345678;
printf("%#x %#x\n",st.b,st.c);
#if 0
int a=0x12345678;
//printf("%#x %#x\n",(char)a,(short)a);
char *p=(char *)&a;
printf("%#x %#x\n",*p,*(p+3));
#endif
return 0;
}
8.1 主机字节序到网络字节序
u_long htonl (u_long hostlong);//host to net
u_short htons (u_short short); //掌握这个
8.2 网络字节序到主机字节序
u_long ntohl (u_long hostlong);//net to host
u_short ntohs (u_short short);
8.3 IP地址转换
typedef uint32_t in_addr_t;
struct in_addr {
in_addr_t s_addr;
};
in_addr_t inet_addr(const char *cp); //从人看的ip地址转为机器使用的32位无符号整数
char *inet_ntoa(struct in_addr in); //从机器到人,即net网络转addr地址
int inet_aton(const char *cp, struct int_addr *inp);//从人到机器
如:inet_aton(“192.168.1.0”,& mysock.sin_addr.s_addr); //设置地址
思考:Dos(拒绝式服务)攻击? (查阅)
拒绝服务攻击即是攻击者想办法让目标机器停止提供服务,是黑客常用的攻击手段之一。其实对网络带宽进行的消耗性攻击只是拒绝服务攻击的一小部分,只要能够对目标造成麻烦,使某些服务被暂停甚至主机死机,都属于拒绝服务攻击。拒绝服务攻击问题也一直得不到合理的解决,究其原因是因为网络协议本身的安全缺陷,从而拒绝服务攻击也成为了攻击者的终极手法。攻击者进行拒绝服务攻击,实际上让服务器实现两种效果:一是迫使服务器的缓冲区满,不接收新的请求;二是使用IP欺骗,迫使服务器把非法用户的连接复位,影响合法用户的连接。
九、TCP编程
9.1 三次握手流程图
服务器:
socket:创建一个用与链接的套接字
bind:绑定自己的ip地址和端口
listen:监听,将主动套接字转为被动套接字
accept:阻塞等待客户端链接,链接成功返回一个用于通信套接字
recv:接收消息
send:发送消息
close:关闭文件描述符
客户端:
socket:创建一个套接字
填充结构体:填充服务器的ip和端口
connect:阻塞等待链接服务器
recv/send:接收/发送消息
close:关闭
9.2 函数接口
1、 socket创建套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
domain:协议族
AF_UNIX, AF_LOCAL 本地通信协议 unix(7)
AF_INET IPv4 ip(7)
AF_INET6 IPv6 ipv6(7)
type:套接字类型
SOCK_STREAM 流式套接字
SOCK_DGRAM 数据报套接字
SOCK_RAW 原始套接字
protocol:协议 填0,自动匹配底层TCP或UDP等协议
系统默认自动帮助匹配对应协议
传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
返回值:成功文件描述符 失败-1 更新errno
2、 bind绑定套接字
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:绑定套接字 - ip和端口
功能:
sockfd:套接字文件描述符
addr:用于通信结构体 (提供的是通用结构体,需要根据选择通信方式,
填充对应结构体-通信结构体由socket第一个参数确定)
addrlen:结构体大小
返回值: 成功:0
失败:-1 更新errno
通用结构体:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
ipv4的通信结构体:
struct sockaddr_in {
sa_family_t sin_family; /*AF_INET */
in_port_t sin_port; /* 端口 */
struct in_addr sin_addr; /* ip地址 */
};
struct in_addr {
uint32_t s_addr;
};
本地通信结构体:
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* 套接字文件 */
};
IPV4结构体使用方式一:
IPV4结构体使用方式二:
IPV4结构体使用方式三:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main(int argc,char **argv)
{
int sockfd;
struct sockaddr_in mysock;
sockfd = socket(AF_INET,SOCK_STREAM,0); //获得fd
bzero(&mysock,sizeof(mysock)); //初始化结构体
mysock.sin_family = AF_INET; //设置地址家族
mysock.sin_port = htons(8080); //设置端口
inet_aton(“192.168.1.0”,& mysock.sin_addr.s_addr); //设置地址
// mysock.sin_addr.s_addr = inet_addr("192.168.1.0");
bind(sockfd,(struct sockaddr *)&mysock,sizeof(struct sockaddr); /* bind的时候进行转化 */
... ...
return 0;
}
3、 listen监听
int listen(int sockfd, int backlog);
功能:监听,将主动套接字变为被动套接字,创建等待队列,用来存放未处理的客户连接请求。
参数:
sockfd:套接字
backlog:同时响应客户端请求链接的最大个数,不能写0.
不同平台可同时链接的数不同,一般写6-8个
(队列1:保存正在连接)
(队列2,连接上的客户端)
返回值:成功 0 失败-1,更新errno
4、 accept等待连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd,NULL,NULL);
阻塞函数,阻塞等待客户端的连接请求,如果有客户端连接,
则accept()函数返回,返回一个用于通信的套接字文件;
参数:
Sockfd :套接字
addr: 链接客户端的ip和端口号
如果不需要关心具体是哪一个客户端,那么可以填NULL;
addrlen:结构体的大小
如果不需要关心具体是哪一个客户端,那么可以填NULL;
返回值:
成功:文件描述符; //用于通信
失败:-1,更新errno
.会创建一个新的套接字文件描述符,后面的recv就是对新的套接字文件描述符操作。
5、 recv接收消息
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能: 接收数据
参数:
sockfd: acceptfd ;
buf 存放位置
len 大小
flags 一般填0,相当于read()函数
MSG_DONTWAIT 非阻塞
返回值:
< 0 失败出错 更新errno
==0 表示客户端退出
>0 成功接收的字节个数
6、 send发送消息
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据
参数:
sockfd:socket函数的返回值
buf:发送内容存放的地址
len:发送内存的长度
flags:如果填0,相当于write();
返回值:
< 0 失败出错 更新errno
>0 成功发送的字节个数
7、 connect发送连接
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:用于连接服务器;
参数:
sockfd:socket函数的返回值
addr:填充的结构体是服务器端的;
addrlen:结构体的大小
返回值
-1 失败,更新errno
正确 0
需要指明客户端是哪一个,accept如果不关心是哪个客户端,可以NULL忽略。
注:发送数据的大小最好与接收数据的大小一致。
server.c:
server.c:
8、 优化代码
1.去掉fget获取多余的‘\n’
if(buf[strlen(buf)-1]=='\n')
buf[strlen(buf)-1]='\0';
2.端口和ip地址通过命令行传参到代码中。
3.设置客户端退出,服务器结束循环接收。
通过recv返回值为0判断客户端是否退出
4.设置来电显示功能,获取到请求链接服务器的客户端的ip和端口。
5.设置服务器端自动获取自己的ip地址。
INADDR_ANY "0.0.0.0"
6.实现循环服务器,服务器不退出,当链接服务器的客户端退出,服务器等到下一个客户端链接。
十、实现:tcp实现ftp功能
模拟FTP核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在,那么清空文件然后写入。
项目功能介绍:
均有服务器和客户端代码,基于TCP写的。
在同一路径下,将客户端可执行代码复制到其他的路径下,接下来再不同的路径下运行服务器和客户端。
相当于另外一台电脑在访问服务器。
客户端和服务器链接成功后出现以下提示:四个功能
***************list************** //列出服务器所在目录下的文件名(除目录不显示)
***********put filename********** //上传一个文件
***********get filename********** //重服务器所在路径下载文件
**************quit*************** //退出(可只退出客户端,服务器等待下一个客户端链接)
10.1 IO相关功能回顾
10.2 代码实现
server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int server_list(int acceptfd);
int server_get(int acceptfd, char *file_p);
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("please input %s <port>\n", argv[0]);
return -1;
}
//1.创建套接字 socket
int sockfd, acceptfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket ok %d\n", sockfd);
//填充ipv4的通信结构体
struct sockaddr_in serveraddr, caddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[1]));
//serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
// serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_addr.s_addr = INADDR_ANY;
socklen_t len = sizeof(caddr);
//&serveraddr -->struct sockaddr_in *
//2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己)
if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("bind err.");
return -1;
}
printf("bind ok.\n");
//3.listen 监听 将主动套接子变为被动等待
if (listen(sockfd, 5) < 0)
{
perror("listen err.");
return -1;
}
printf("listen ok.\n");
while (1)
{
//4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err.");
return -1;
}
printf("accept ok.\n");
//输出查看链接的客户端的ip和端口
printf("ip:%s ,port:%d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
//5.循环收发消息
char buf[128];
int recvbyte;
while (1)
{
//接收消息
recvbyte = recv(acceptfd, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err.");
return -1;
}
else if (recvbyte == 0)
{
printf("client exit.\n");
break;
}
else
{
printf("buf:%s\n", buf);
switch (buf[0])
{
case 'l':
server_list(acceptfd);
break;
case 'g':
server_get(acceptfd, buf + 4);
break;
}
}
}
close(acceptfd);
}
close(sockfd);
return 0;
}
//get filename
//打开已存在的文件,读文件内容发送给客户端
int server_get(int acceptfd, char *file_p)
{
char buf[128];
int ret;
int fd = open(file_p, O_RDONLY);
if (fd < 0)
{
perror("open error.");
return -1;
}
while (1)
{
ret=read(fd, buf, sizeof(buf)-1);//预留一个位置补\0
buf[ret]='\0';
if(ret == 0)
break;
send(acceptfd, buf, sizeof(buf), 0);
}
strcpy(buf, "send ok");
send(acceptfd, buf, sizeof(buf), 0);
return 0;
}
int server_list(int acceptfd)
{
char buf[128];
struct stat st;
struct dirent *file = NULL;
DIR *dir = opendir("./");
if (dir == NULL)
{
perror("opendir err.");
return -1;
}
while ((file = readdir(dir)) != NULL)
{
stat(file->d_name, &st);
if ((st.st_mode & S_IFMT) == S_IFREG)
{
strcpy(buf, file->d_name);
send(acceptfd, buf, sizeof(buf), 0);
}
}
strcpy(buf, "send ok");
send(acceptfd, buf, sizeof(buf), 0);
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int client_list(int sockfd);
void list(void);
int client_get(int sockfd, char *file_p);
int main(int argc, char const *argv[])
{
if (argc != 3)
{
printf("please input %s <ip> <port>\n", argv[0]);
return -1;
}
//1.创建套接字 socket
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket ok %d\n", sockfd);
//填充ipv4的通信结构体 服务器端ip和端口
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
//2.请求链接 connect
if (connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("connect err.");
return -1;
}
printf("connect ok.\n");
//3.循环收发消息
char buf[128];
int recvbyte;
while (1)
{
list();
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
if (strncmp(buf, "quit", 4) == 0)
break;
//发送请求
send(sockfd, buf, sizeof(buf), 0);
switch (buf[0])
{
case 'l':
client_list(sockfd);
break;
case 'g':
client_get(sockfd, buf + 4);
break;
}
}
close(sockfd);
return 0;
}
//get filename
//打开新建一个文件,接收写文件
int client_get(int sockfd, char *file_p)
{
char buf[128];
//1.打开新建文件
int fd = open(file_p, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open err.");
return -1;
}
//2.循环接收写文件
while (1)
{
recv(sockfd, buf, sizeof(buf), 0);
if (strncmp(buf, "send ok", 7) == 0)
break;
//写文件
write(fd,buf,strlen(buf));
}
return 0;
}
//list - 功能:显示服务器对应路径下的普通文件名
// 接收服务器发送过来的普通文件的名字
int client_list(int sockfd)
{
char buf[128] = "";
while (1)
{
recv(sockfd, buf, sizeof(buf), 0);
if (strncmp(buf, "send ok", 7) == 0)
break;
printf("%s\n", buf);
}
return 0;
}
void list(void)
{
printf("+++++++++++++++++++++++++++++++++++++++\n");
printf("+++++++++++++ list +++++++++++++++++\n");
printf("+++++++++++++ get filename ++++++++++\n");
printf("+++++++++++++ put filename ++++++++++\n");
printf("+++++++++++++ quit ++++++++++++++++\n");
printf("+++++++++++++++++++++++++++++++++++++++\n");
}
十一、UDP编程
11.1 UDP流程图
udp流程:(类似发短信)
server:
创建数据报套接字(socket(,SOCK_DGRAM,))----->有手机
绑定网络信息(bind())---------------------->绑定号码(发短信知道发给谁)
接收信息(recvfrom())--------------------->接收短信
关闭套接字(close())----------------------->接收完毕
client:
创建数据报套接字(socket())----------------------->有手机
指定服务器的网络信息------------------------------>有对方号码
发送信息(sendto())---------------------------->发送短信
关闭套接字(close())--------------------------->发送完
11.2 函数接口
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
功能:接收数据
参数:
sockfd:套接字描述符
buf:接收缓存区的首地址
len:接收缓存区的大小
flags:0
src_addr:发送端的网络信息结构体的指针
addrlen:发送端的网络信息结构体的大小的指针
返回值:
成功接收的字节个数
失败:-1
0:客户端退出
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
功能:发送数据
参数:
sockfd:套接字描述符
buf:发送缓存区的首地址
len:发送缓存区的大小
flags:0
src_addr:接收端的网络信息结构体的指针
addrlen:接收端的网络信息结构体的大小
返回值:
成功发送的字节个数
失败:-1
server.c
client.c
练习:实现如客户端发送"hello"给服务器端,服务器接着给客户端回,“recv:hello!!!”。
十二、UDP网络聊天室
注意:
1、对于TCP是先运行服务器,客户端才能运行。
2、对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系,
3、一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。
4、UDP,客户端当使用send的时候,上面需要加connect,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。
5、在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个参数都写为NULL就OK。
12.1 项目要求
利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。
12.2 问题思考
12.2 问题思考
● 客户端会不会知道其它客户端地址?
UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。
● 有几种消息类型?
**登录:**服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
**聊天:**服务器只需要把某个客户端的聊天消息转发给所有其它客户端。
**退出:**服务器删除退出客户端的地址,并把退出消息发送给其它客户端。
● 服务器如何存储客户端的地址?
数据结构可以选择线性数据结构
链表节点结构体:
struct node{
struct sockaddr_in addr;
struct node *next;
};
消息对应的结构体(同一个协议)
typedef struct msg_t
{
int type;//L M Q
char name[32];//用户名
char text[128];//消息正文
}MSG_t;
● 客户端如何同时处理发送和接收?
客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。
12.3 程序流程图
服务器端
客户端
十三、linux下I/O模型及特点
13.1 阻塞式IO
特点:最简单、最常用;效率低
阻塞I/O 模式是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
缺省情况下(及系统默认状态),套接字建立后所处于的模式就是阻塞I/O 模式。
学习的读写函数在调用过程中会发生阻塞相关函数如下:
•读操作中的read、recv、recvfrom
读阻塞--》需要读缓冲区中有数据可读,读阻塞解除
•写操作中的write、send
写阻塞--》阻塞情况比较少,主要发生在写入的缓冲区的大小小于要写入的数据量的情况下,写操作不进行任何拷贝工作,将发生阻塞,一旦缓冲区有足够的空间,内核将唤醒进程,将数据从用户缓冲区拷贝到相应的发送数据缓冲区。
注意:sendto没有写阻塞
1)无sendto函数的原因:
sendto不是阻塞函数,本身udp通信不是面向链接的,udp无发送缓冲区,即sendto没有发送缓冲区,send是有发送缓存区的,即sendto不是阻塞函数。
2)UDP不用等待确认,没有实际的发送缓冲区,所以UDP协议中不存在缓冲区满的情况,在UDP套接字上进行写操作永远不会阻塞。
•其他操作:accept、connect
13.2 非阻塞式IO
特点:可以处理多路IO;需要轮询,浪费CPU资源
•当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。
•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
•这种模式使用中不普遍。
13.2.1 通过函数参数设置非阻塞:
Recv函数最后一个参数写为0,为阻塞,写为MSG_DONTWAIT:表示非阻塞。
非阻塞,循环检测,是否有数据发过来,轮询消耗CPU资源。
13.2.2 通过fcntl函数设置文件描述符属性设置非阻塞:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置文件描述符的属性
参数:
fd:文件描述符
cmd:功能选择 - 更改状态属性
F_SETFLF 设置文件描述符属性 第三个参设设置
F_GETFL 获取文件描述符属性 第三个参数忽略 返回的是获取到的属性
arg:设置的属性值
返回值:失败 -1 更新errno
成功根据功能选择返回
fcntl.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
char buf[128];
//设置文件描述符0的非阻塞属性
int flags;
flags = fcntl(0, F_GETFL); //获取原属性
flags |= O_NONBLOCK; //修改属性
fcntl(0, F_SETFL, flags); //修改的属性设置回去
while (1)
{
fgets(buf, sizeof(buf), stdin);
printf("buf:%s\n", buf);
}
return 0;
}
13.3 信号驱动IO (异步IO模型 非重点)
特点:异步通知模式,需要底层驱动的支持
● 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。
● 应用程序收到信号后做异步处理即可。
● 应用程序需要把自己的进程号告诉内核,并打开异步通知机制。
标准模板
//设置将APP进程号提交给内核驱动
fcntl(fd,F_SETOWN,getpid());
//设置异步通知
int flags;
flags = fcntl(fd, F_GETFL); //获取原属性
flags |= O_ASYNC; //设置异步通知
fcntl(fd, F_SETFL, flags); //修改的属性设置回去
signal(SIGIO,handler);
举例:操作鼠标设备,当有输入的时候获取输入数据,没有输入时循环输出hello world。
鼠标设备路径 - /dev/input/mouse0
测试使用的是哪个鼠标设备:sudo cat /dev/input/mouse0
注意:执行代码时需要加sudo,普通用户没有操作设备的权限。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
int fd;
//信号处理函数
void handler(int sig)
{
char buf[128];
int ret = read(fd, buf, sizeof(buf) - 1);
buf[ret] = '\0';
printf("mouse:%s\n", buf);
}
int main(int argc, char const *argv[])
{
//1.打开鼠标设备
fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open mouse err.");
return -1;
}
//2.将APP进程号告诉内核驱动
fcntl(fd, F_SETOWN, getpid());
//3.设置异步通信
int flags;
flags = fcntl(fd, F_GETFL); //获取原属性
flags |= O_ASYNC; //设置异步通知
fcntl(fd, F_SETFL, flags); //修改的属性设置回去
//4.signal捕捉SIGIO信号
signal(SIGIO, handler);
while (1)
{
printf("hello world.\n");
sleep(1);
}
return 0;
}
13.4 前三种使用场景假设总结
假设妈妈有一个孩子,孩子在房间里睡觉,妈妈需要及时获知孩子是否醒了,如何做?
- 进到房间陪着孩子一起睡觉,孩子醒了会吵醒妈妈:不累,但是不能干别的了
- 时不时进房间看一下:简单,空闲时间还能干点别的,但是很累
- 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:互不耽误
13.5 IO多路复用
13.5.1 IO多路复用场景假设
假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?
- 不停进每个房间看一下:简单,空闲时间还能干点别的,但是很累
- 把三个房间的门都打开,在客厅睡觉,同时监听所有房间的哭声,如果被哭声吵醒,那么能准确定位某个房间,及时处理即可:既能得到休息,也能及时获知每个孩子的状态。
13.5.2 IO多路复用机制
● 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;
● 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
● 若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;
● 比较好的方法是使用I/O多路复用技术。其基本思想是:
○ 先构造一张有关描述符的表(最大1024),然后调用一个函数。
○ 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
○ 函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。
基本流程:
1. 先构造一张有关文件描述符的表(集合、数组);
2. 将你关心的文件描述符加入到这个表中;
3. 然后调用一个函数。 select / poll
4. 当这些文件描述符中的一个或多个已准备好进行I/O操作的时候
该函数才返回(阻塞)。
5. 判断是哪一个或哪些文件描述符产生了事件(IO操作);
6. 做对应的逻辑处理;
13.5.3 实现IO多路复用的方式
13.5.3.1 select 实现IO多路复用
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:select用于监测是哪个或哪些文件描述符产生事件;
有一个或多个同时产生时间返回值。
参数:nfds: 监测的最大文件描述个数
(这里是个数,使用的时候注意,与文件中最后一次打开的文件
描述符所对应的值的关系是什么?)
readfds: 读事件集合; //读(用的多)
writefds: 写事件集合; //NULL表示不关心
exceptfds:异常事件集合;
timeout:超时检测 1
如果不做超时检测:传 NULL
select返回值: <0 出错
>0 表示有事件产生;
如果设置了超时检测时间:&tv
select返回值:
<0 出错
>0 表示有事件产生;
==0 表示超时时间已到;
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_CLR(int fd, fd_set *set);//将fd从表中清除
int FD_ISSET(int fd, fd_set *set);//判断fd是否在表中
void FD_SET(int fd, fd_set *set);//将fd添加到表中
void FD_ZERO(fd_set *set);//清空表
select实现IO多路复用特点
1. 一个进程最多只能监听1024个文件描述符 (千级别)
2. select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源);
3. select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程0~4G,0~3G是用户态,3G~4G是内核态,拷贝是非常耗时的);
练习:检测鼠标输入和键盘输入事件。
moues_key_select.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
//打开鼠标设备
int fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open mouse err.");
return -1;
}
//select 实现同时响应终端输入和鼠标输入
//1.创建表
fd_set readfds,tempfds;
//清空表
FD_ZERO(&readfds);
//2.将关心的文件描述符添加到表中
FD_SET(fd,&readfds);
FD_SET(0,&readfds);
int maxfd=fd;
char buf[128];
int ret;
while(1)
{
//3.调用select函数检测事件
tempfds=readfds;
ret=select(maxfd+1,&tempfds,NULL,NULL,NULL);
if(ret < 0)
{
perror("select err.");
return -1;
}
//终端输入
if(FD_ISSET(0,&tempfds))
{
fgets(buf,sizeof(buf),stdin);
printf("key:%s\n",buf);
}
//鼠标输入
if(FD_ISSET(fd,&tempfds))
{
int ret=read(fd,buf,sizeof(buf));
printf("mouse:%s\n",buf);
}
}
close(fd);
return 0;
}
练习2:检测键盘和sockfd (TCP实现同时链接多个客户端)
修改 serever.c代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("please input %s <port>\n", argv[0]);
return -1;
}
//1.创建套接字 socket
int sockfd, acceptfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket ok %d\n", sockfd);
//填充ipv4的通信结构体
struct sockaddr_in serveraddr, caddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[1]));
//serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
// serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_addr.s_addr = INADDR_ANY;
socklen_t len = sizeof(caddr);
//&serveraddr -->struct sockaddr_in *
//2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己)
if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("bind err.");
return -1;
}
printf("bind ok.\n");
//3.listen 监听 将主动套接子变为被动等待
if (listen(sockfd, 5) < 0)
{
perror("listen err.");
return -1;
}
printf("listen ok.\n");
//select - 多个客户端链接一个服务器 用于链接的文件描述符是sockfd
//请求,sockfd读到请求 检测sockfd的读事件
//1.创建表
fd_set readfds,tempfds;
//清空表
FD_ZERO(&readfds);
//2.将关心文件描述符添加到表 0 sockfd=3
FD_SET(0,&readfds);
FD_SET(sockfd,&readfds);
int maxfd=sockfd;
//3.循环调用select检测
int ret;
char buf[128];
while (1)
{
tempfds=readfds;
ret=select(maxfd+1,&tempfds,NULL,NULL,NULL);
if(ret < 0)
{
perror("select err.");
return -1;
}
//4.判断是否产生事件
if(FD_ISSET(0,&tempfds))
{
fgets(buf,sizeof(buf),stdin);
printf("key:%s\n",buf);
}
if(FD_ISSET(sockfd,&tempfds))
{
//sockfd产生事件,有客户端链接,需要调用accept进行链接
//4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err.");
return -1;
}
printf("accept ok.\n");
//输出查看链接的客户端的ip和端口
printf("%d ip:%s ,port:%d\n",acceptfd,
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
}
}
close(sockfd);
return 0;
}
练习3:在实现练习2的基础上,再检测所有用于通信的acceptfd实现并发通信。
server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("please input %s <port>\n", argv[0]);
return -1;
}
//1.创建套接字 socket
int sockfd, acceptfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket ok %d\n", sockfd);
//填充ipv4的通信结构体
struct sockaddr_in serveraddr, caddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[1]));
//serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
// serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_addr.s_addr = INADDR_ANY;
socklen_t len = sizeof(caddr);
//&serveraddr -->struct sockaddr_in *
//2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己)
if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("bind err.");
return -1;
}
printf("bind ok.\n");
//3.listen 监听 将主动套接子变为被动等待
if (listen(sockfd, 5) < 0)
{
perror("listen err.");
return -1;
}
printf("listen ok.\n");
//功能:select 服务器响应多个客户端请求且进行通信
//1.创建表
fd_set readfds, tempfds;
FD_ZERO(&readfds); //清空表
//2.添加关心的文件描述符
FD_SET(0, &readfds);
FD_SET(sockfd, &readfds);
int maxfd = sockfd, ret;
char buf[128];
int recvbyte;
//3.循环调用select检测 ,select阻塞等待有事件产生返回
while (1)
{
tempfds = readfds;
ret = select(maxfd + 1, &tempfds, NULL, NULL, NULL);
if (ret < 0)
{
perror("select err.");
return -1;
}
for (int i = 0; i <= maxfd; i++)
{
if (FD_ISSET(i, &tempfds))//循环遍历i即文件描述符的值,判断是否在监测表中
{
if (i == 0)//监测标准输入
{
fgets(buf, sizeof(buf), stdin);
printf("key:%s\n", buf);
//将服务器段输入的数据作为通知的消息发送给所有链接的客户端
for(int j=4;j<=maxfd;j++)
{
if(FD_ISSET(j,&readfds))
{
send(j,buf,sizeof(buf),0);
}
}
}
else if (i == sockfd)//监测sockfd
{
//4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err.");
return -1;
}
printf("accept ok.\n");
//输出查看链接的客户端的ip和端口
printf("ip:%s ,port:%d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
//需要将产生通信文件描述符添加到表中进行检测
FD_SET(acceptfd, &readfds);
if (maxfd < acceptfd)
maxfd = acceptfd;
}
else//监测所有acceptfd
{
//接收消息
recvbyte = recv(i, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err.");
// return -1;
}
else if (recvbyte == 0)
{
printf("%d client exit.\n",i);
FD_CLR(i, &readfds);//从文件描述符表中清除对应fd
close(i);
break;
}
else
{
printf("%d buf:%s\n",i, buf);
}
}
}
}
}
close(sockfd);
return 0;
}
client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
int main(int argc, char const *argv[])
{
if (argc != 3)
{
printf("please input %s <ip> <port>\n", argv[0]);
return -1;
}
//1.创建套接字 socket
int sockfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket ok %d\n", sockfd);
//填充ipv4的通信结构体 服务器端ip和端口
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
//&serveraddr -->struct sockaddr_in *
//2.请求链接 connect
if (connect(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("connect err.");
return -1;
}
printf("connect ok.\n");
//3.循环收发消息
char buf[128];
int recvbyte;
pid_t pid = fork();//通过子进程实现全双工通信
if (pid < 0)
{
perror("fork err.");
return -1;
}
else if (pid == 0)
{
while (1)
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
//发送消息
send(sockfd, buf, sizeof(buf), 0);
}
}
else
{
while (1)
{
//接收消息
recvbyte = recv(sockfd, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err.");
// return -1;
}
printf("buf:%s\n", buf);
}
}
close(sockfd);
return 0;
}
13.5.3.2 poll 实现IO多路复用
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参p数:
struct pollfd *fds
关心的文件描述符数组struct pollfd fds[N];
nfds:个数
timeout: 超时检测
毫秒级的:如果填1000,1秒
如果-1,阻塞
struct pollfd {
int fd; /* 检测的文件描述符 */
short events; /* 检测事件 */
short revents; /* 调用poll函数返回填充的事件,poll函数一旦返回,将对应事件自动填充结构体这个成员。只需要判断这个成员的值就可以确定是否产生事件 */
};
使用如:
struct pollfd fds[20]={};//poll()函数第一个参数传递的是fds的地址
事件: POLLIN :读事件
POLLOUT : 写事件
POLLERR:异常事件
poll实现IO多路复用的特点
1. 优化文件描述符个数的限制;(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组元素个数为1,如果想监听100个,那么这个结构体数组的元素个数就为100,由程序员自己来决定)
2. poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低
3. poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可
server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <poll.h>
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("please input %s <port>\n", argv[0]);
return -1;
}
//1.创建套接字 socket
int sockfd, acceptfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket ok %d\n", sockfd);
//填充ipv4的通信结构体
struct sockaddr_in serveraddr, caddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[1]));
//serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
// serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_addr.s_addr = INADDR_ANY;
socklen_t len = sizeof(caddr);
//&serveraddr -->struct sockaddr_in *
//2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己)
if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("bind err.");
return -1;
}
printf("bind ok.\n");
//3.listen 监听 将主动套接子变为被动等待
if (listen(sockfd, 5) < 0)
{
perror("listen err.");
return -1;
}
printf("listen ok.\n");
//功能:poll 服务器响应多个客户端请求且进行通信
//1.创建表 -创建一个结构体数组
struct pollfd fds[20]={}; //大小自己确定,没有限定个数
//2.添加关心的文件描述符
fds[0].fd = 0;
fds[0].events=POLLIN;//读事件
fds[1].fd=sockfd;
fds[1].events=POLLIN;
int n = 2, ret;
char buf[128];
int recvbyte;
//3.循环调用poll检测 ,poll阻塞等待有事件产生返回 -1
while (1)
{
ret = poll(fds, n, -1);
if (ret < 0)
{
perror("poll err.");
return -1;
}
//处理事件
for (int i = 0; i < n; i++)
{
if (fds[i].revents == POLLIN)
{
if (fds[i].fd == 0)
{
fgets(buf, sizeof(buf), stdin);
printf("key:%s\n", buf);
//将服务器段输入的数据作为通知的消息发送给所有链接的客户端
for(int j=2;j<n;j++)
{
send(fds[j].fd,buf,sizeof(buf),0);
}
}
else if (fds[i].fd == sockfd)
{
//4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err.");
return -1;
}
printf("accept ok.\n");
//输出查看链接的客户端的ip和端口
printf("ip:%s ,port:%d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
//需要将产生通信文件描述符添加到表中进行检测
fds[n].fd=acceptfd;
fds[n].events=POLLIN;
n++;
}
else
{
//接收消息
recvbyte = recv(fds[i].fd, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err.");
// return -1;
}
else if (recvbyte == 0)
{
printf("%d client exit.\n",fds[i].fd);
close(fds[i].fd);
fds[i]=fds[n-1];
n--;
i--;
break;
}
else
{
printf("%d buf:%s\n",fds[i].fd, buf);
}
}
}
}
}
close(sockfd);
return 0;
}
13.5.3.3 epoll 实现IO多路复用(异步)
epoll实现机制:(了解)
epoll的提出--》它所支持的文件描述符上限是系统可以最大打开的文件的数目;eg:1GB机器上,这个上限10万个左右。
每个fd上面有callback(回调函数)函数,只有活跃的fd才有主动调用callback,不需要轮询。
注意:
Epoll处理高并发,百万级,不关心底层怎样实现,只需要会调用就可以。
函数接口
#include <sys/epoll.h>
int epoll_create(int size);
功能:创建红黑树根节点
参数:size:不作为实际意义值 >0 即可
返回值:成功时返回epoll文件描述符,失败时返回-1。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制epoll属性
epfd:epoll_create函数的返回句柄。
op:表示动作类型。有三个宏 来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已注册fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
Fd:需要监听的fd。
event:告诉内核需要监听什么事件
EPOLLIN:表示对应文件描述符可读
EPOLLOUT:可写
EPOLLPRI:有紧急数据可读;
EPOLLERR:错误;
EPOLLHUP:被挂断;
EPOLLET:触发方式,边缘触发;(默认使用边缘触发)
ET模式:表示状态的变化;
返回值:成功时返回0,失败时返回-1
typedef union epoll_data{
void* ptr;(无效)
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; / * Epoll事件* /
epoll_data_t data; / *用户数据变量* /
};
//等待事件到来
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能:等待事件的产生,类似于select的用法
epfd:句柄;
events:用来保存从内核得到事件的集合;
maxevents:表示每次能处理事件最大个数;
timeout:超时时间,毫秒,0立即返回,-1阻塞
成功时返回发生事件的文件描述个数,失败时返回-1
帮助理解:
1.epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
2.epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时, 返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中 依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。
3.另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()
来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制, 迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。
epoll实现IO多路复用的特点
•监听的最大的文件描述符没有个数限制(理论上,取决与你自己的系统)
•异步I/O,Epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高
•epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.
server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <poll.h>
#include <sys/epoll.h>
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("please input %s <port>\n", argv[0]);
return -1;
}
//1.创建套接字 socket
int sockfd, acceptfd;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
printf("socket ok %d\n", sockfd);
//填充ipv4的通信结构体
struct sockaddr_in serveraddr, caddr;
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[1]));
//serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0"); //0.0.0.0 自动获取主机ip
// serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_addr.s_addr = INADDR_ANY;
socklen_t len = sizeof(caddr);
//&serveraddr -->struct sockaddr_in *
//2.绑定套接字 bind (绑定自己的ip和端口,便于别人找到自己)
if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("bind err.");
return -1;
}
printf("bind ok.\n");
//3.listen 监听 将主动套接子变为被动等待
if (listen(sockfd, 5) < 0)
{
perror("listen err.");
return -1;
}
printf("listen ok.\n");
//功能:epoll 服务器响应多个客户端请求且进行通信
struct epoll_event event; //暂时保存添加到树上的事件
struct epoll_event revents[20]; //保存从链表中获取的产生的事件
//1.创建表 -创建一颗树
int epfd = epoll_create(1);
//2.添加关心的文件描述符到树上 epoll_ctl
event.events = EPOLLIN | EPOLLET;
event.data.fd = 0;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);
event.events = EPOLLIN | EPOLLET;
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
int n = 2, ret;
char buf[128];
int recvbyte;
//3.循环调用epoll_wait检测链表中是否有事件
while (1)
{
//返回值是实际从链表中拿出来事件的个数
ret = epoll_wait(epfd, revents, 20, -1);
if (ret < 0)
{
perror("poll err.");
return -1;
}
//处理事件
for (int i = 0; i < ret; i++)
{
if (revents[i].data.fd == 0)
{
fgets(buf, sizeof(buf), stdin);
printf("key:%s\n", buf);
}
else if (revents[i].data.fd == sockfd)
{
//4.阻塞等待客户端链接,链接成功返回一个通信文件描述符 accept
acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err.");
return -1;
}
printf("accept ok.\n");
//输出查看链接的客户端的ip和端口
printf("ip:%s ,port:%d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
//需要将产生通信文件描述符添加到树
event.events = EPOLLIN | EPOLLET;
event.data.fd = acceptfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &event);
}
else
{
//接收消息
recvbyte = recv(revents[i].data.fd, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err.");
// return -1;
}
else if (recvbyte == 0)
{
printf("%d client exit.\n", revents[i].data.fd);
close(revents[i].data.fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, revents[i].data.fd,NULL);
break;
}
else
{
printf("%d buf:%s\n",revents[i].data.fd, buf);
}
}
}
}
close(sockfd);
return 0;
}
十四、服务器模型
● 在网络程序里面,通常都是一个服务器处理多个客户机。
● 为了处理多个客户机的请求, 服务器端的程序有不同的处理方式。
14.1 循环服务器模型
同一个时刻只能响应一个客户端的请求,伪代码如下:
socket()
bind();
listen();
while(1)
{
accept();
while(1)
{
process(); //处理
}
close();
}
14.2 并发服务器模型
同一个时刻可以响应多个客户端的请求,常用的模型有多进程模型/多线程模型/IO多路复用模型。
14.2.1 多进程模型
每来一个客户端连接,开一个子进程来专门处理客户端的数据,实现简单,但是系统开销相对较大,更推荐使用线程模型。伪代码如下:
socket()
bind();
listen();
while(1)
{
accept();
if(fork() == 0) //子进程
{
while(1)
{
process();
}
close(client_fd);
exit();
}
else
{
}
}
注意:收到客户端消息后,打印下是来自哪个客户端的数据(来电显示)
使用SIGCHLD来处理子进程结束的信号,信号函数中回收进程资源。
14.2.2 多线程模型
来一个客户端连接,开一个子线程来专门处理客户端的数据,实现简单,占用资源较少,属于使用比较广泛的模型:
socket()
bind();
listen();
while(1)
{
accept();
pthread_create();
}
14.2.3 IO多路复用模型
借助select、poll、epoll机制,将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接,在嵌入式开发中应用广泛,不过代码写起了稍显繁琐。