一、认识网络
1、网络发展史
网络的来历_百度知道
ARPnetA--Internet--移动互联网--物联网
2、局域网和广域网
局域网(LAN)
局域网的缩写是LAN,local area network,顾名思义,是个本地的网络,只能实现小范围短距离的网络通信。我们的家庭网络是典型的局域网。电脑、手机、电视、智能音箱、智能插座都连在路由器上,可以互相通信。局域网,就像是小区里的道路,分支多,连接了很多栋楼。
广域网(Wan)
广域网(Wide Area Network)是相对局域网来讲的,局域网的传输距离比较近,只能是一个小范围的。如果需要长距离的传输,比如某大型企业,总部在北京,分公司在长沙,局域网是无法架设的。广域网,就像是大马路,分支可能少,但类型多,像国道、省道、高速、小道等,连接了很多大的局域网。
这时需要其它的解决方案。
第一,通过因特网,只需要办一根宽带,就实现了通信,非常方便,现在的宽带价格也比较便宜。
第二,通过广域网专线。
所以为了数据安全,不能连接因特网,需要用一条自己的专用线路来传输数据,这条线路上只有自己人,不会有其他人接入,且距离很远,这个网络就叫 “广域网”。
3、光猫
光猫是一种类似于基带modem(数字调制解调器)的设备,和基带modem不同的是接入的是光纤专线,是光信号。用于广域网中光电信号的转换和接口协议的转换,接入路由器,是广域网接入。
将光线插入左侧的灰色口,右侧网口接网线到路由器即可。
4、交换机与路由器
交换机(二层):用于局域网内网的数据转发路由器(三层):用于连接局域网和外网
路由器有交换机的功能,反之不成立,交换机没有IP分配和IP寻址的功能。
交换机各个口是平等的,所有接入的设备需要自己配置IP,然后组成局域网。
路由器需要区分WAN口和LAN口,WAN口是接外网的(从Modem出来的或者从上一级路由器出来的),LAN口是接内网的,现在路由器都带无线功能,本质上无线接入就是LAN。
5、网线
背过一种线序,了解网线的制作流程。
网线线序
网线制作教程
6、IP地址
6.1 基本概念
- IP地址是Internet中主机的标识
- Internet中的主机要与别的机器通信必须具有一个IP地址
- IP地址为32位(IPv4)或者128位(IPv6)
- 表示形式:常用点分形式,如202.38.64.10,最后都会转换为一个32位的无符号整数。
6.2 网络号/主机号
6.2.1 地址划分
主机号的第一个和最后一个都不能被使用,第一个作为网段号,最后一个最为广播地址。
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
6.2.2 特殊地址
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。
6.3 子网掩码
IP地址=网络号+主机号,使用子网掩码来进行区分
网络号:表示是否在一个网段内(局域网)
主机号:标识在本网段内的ID,同一局域网不能重复
- 子网掩码:是一个32位的整数,作用是将某一个IP划分成网络地址和主机地址;
- 子网掩码长度是和IP地址长度完全一样;
- 网络号全为1,主机号全为0;
- 公式:网络号=IP & MASK
思考一:上图中B类地址的子网掩码怎么写?
思考二:B类地址,同一网段最多可以连接多少个主机?
思考三:已知一个子网掩码号为255.255.255.192,问,最多可以连接多少台主机?
7、网络模型
7.1 网络的体系结构
- 网络采用分而治之的方法设计,将网络的功能划分为不同的模块,以分层的形式有机组合在一起。
- 每层实现不同的功能,其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务,同时使用下层提供的服务
- 网络体系结构即指网络的层次结构和每层所使用协议的集合
- 两类非常重要的体系结构:OSI与TCP/IP
7.2 OSI模型
- OSI模型是一个理想化的模型,尚未有完整的实现
- OSI模型共有七层
- OSI现阶段只用作教学和理论研究
7.3 TCP/IP模型
网络接口和物理层:屏蔽硬件差异(驱动),向上层提供统一的操作接口。
网络层:提供端对端的传输,可以理解为通过IP寻址机器。
传输层:决定数据交给机器的哪个任务(进程)去处理,通过端口寻址
应用层:应用协议和应用程序的集合
OSI和TCP/IP模型对应关系图
7.4 常见网络协议
网络接口和物理层:ppp:拨号协议(老式电话线上网方式)ARP:地址解析协议 IP-->MACRARP:反向地址转换协议 MAC-->IP
网络层: IP(IPV4/IPV6):网间互连的协议ICMP:网络控制管理协议,ping命令使用IGMP:网络分组管理协议,广播和组播使用
传输层:TCP:传输控制协议UDP:用户数据报协议
应用层:SSH:加密协议telnet:远程登录协议FTP:文件传输协议HTTP:超文本传输协议DNS:地址解析协议SMTP/POP3:邮件传输协议
注意:TCP和IP是属于不同协议栈层的,只是这两个协议属于协议族里最重要的协议,所以协议栈或者模型以之命名了。
8. TCP/UDP
TCP
TCP(即传输控制协议):是一种面向连接的传输层协议,它能提供高可靠性通信(即数据无误、数据无丢失、数据无失序、数据无重复到达的通信)。
适用场景
适合于对传输质量要求较高的通信
在需要可靠数据传输的场合,通常使用TCP协议
MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议
UDP
UDP(User Datagram Protocol)用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。
适用场景
发送小尺寸数据(如对DNS服务器进行IP地址查询时)
适合于广播/组播式通信中。
MSN/QQ/Skype等即时通讯软件的点对点文本通讯以及音视频通讯通常采用UDP协议
9. 编程预备知识
9.1 socket定义
9.2 socket类型
流式套接字(SOCK_STREAM) TCP
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制,避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
数据报套接字(SOCK_DGRAM) UDP
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字(SOCK_RAW)
可以对较低层次协议如IP、ICMP直接访问。
9.4 端口号
- 为了区分一台主机接收到的数据包应该转交给哪个进程来进行处理,使用端口号来区
- TCP端口号与UDP端口号独立
- 端口号一般由IANA (Internet Assigned Numbers Authority) 管理
- 端口用两个字节来表示
众所周知端口:1~1023(1~255之间为众所周知端口,256~1023端口通常由UNIX系统占用)
已登记端口:1024~49151
动态或私有端口:49152~65535
9.5 字节序
小端序(little-endian) - 低序字节存储在低地址
大端序(big-endian)- 高序字节存储在低地址
网络中传输的数据必须使用网络字节序,即大端字节序
面试题:写一个函数,判断当前主机的字节序?
int checkCPU()
{union w{int a;char b;}c;c.a = 1;return (c.b == 1);
}
主机字节序到网络字节序
u_long htonl (u_long hostlong);
u_short htons (u_short short); //掌握这个
网络字节序到主机字节序
u_long ntohl (u_long hostlong);
u_short ntohs (u_short short);
9.6 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); //从机器到人
示例
int main()
{in_addr_t addr;addr = inet_addr("192.168.1.222");printf("addr=0x%x\n", addr);struct in_addr tmp;tmp.s_addr = addr;printf("ip=%s\n", inet_ntoa(tmp));
}
10. 复习
历史:
阿帕网:不能互联不同的主机、不同操作系统,没有纠错功能
TCP/IP:
IP
TCP
IP : A:首位固定为0。1byte网络号,3byte主机号
0000 0000 - 0111 1111 >0-127
123.0000...000 - 1111...111
255.0.0.0
B:首位固定为10。2byte网络号,2byte主机号
128.0 - 191.255.
172.125
255.255.0.0
C:首位固定为110。3byte网络号,1byte主机号
192.0.0-223.255.255
192.168.1
0-255 =》 254
255.255.255.0
子网掩码:网络全为1,主机号全为0.
22位网络号 10位主机号:
10 00...00 10 11...11
子网掩码:255.255.111111 00 .0000 0000
255.255.252.0
ip=网络号+主机号
网络号:是否处于同一网段
主机号: 唯一分配给主机的id
D(组播)E
port:端口。标识进程 udp 和 TCP端口独立
1-1023
>1023
socket - TCP/TP
IO-C b
网络设备 — socket - > fd
TCP流程:
服务器:
1.创建流式套接字socket .返回连接文件描述符
2.绑定(填充通信结构体)bind
3.监听。主动套接字变为被动套接字listen
4.阻塞等待客户端连接accept .返回通信文件描述符
5.收发消息
6.关闭套接字
客户端:
1.创建流式套接字socket
2.填充服务器的通信结构体
3.请求连接connect
4.收发消息
5.关闭套接字
【1】
基础理论:ip port socket 套接字类型 OSI TCP/IP udp TCP
核心编程框架:TCP UDP
UDP可以直接实现并发服务器
TCP-循环服务器
*TCP实现并发服务器。
引入linux IO模型4种:
1.阻塞IO:
特点-最常用、不能处理多路IO,效率低,不需要轮询,不浪费cpu资源
2.非阻塞:特点-不常用、能处理多路IO,需要轮询,耗费CPU
3.信号驱动IO:异步IO,需要底层驱动支持
4.IO多路复用 - 能实现TCP并发
select poll epoll
【2】UPD
服务器:
创建套接字(数据报套接字)
填充服务器的通信结构体
绑定
发收:
sendto
客户端:
创建套接字socket
填充服务器的通信结构体
发送
sendto(sockfd,buf,size,0,(struct sockaddr*)&saddr,
sizeof(saddr));
len=sizeof(caddr);
recvfrom(sockfd,buf,size,0,(struct sockaddr*)&caddr,&len);
非阻塞:
函数自带参数设置
fcntl(fd,功能选择,属性值(int))
F_GETFL F_SETFL F_SETOWN
IO多路复用:select
编程流程:
1.创建表
fd_set readfds,tempfds;
FD_ZERO(&readfds);
2.添加关心文件描述符到表中
FD_SET(0,&readfds);
// FD_SET(sockfd,&reafds);
...
3.调用函数检测
tempfds=readfds;
select(maxfd+1,&tempfds,NULL,NULL,NULL);
4.一个或多个文件描述符有事件产生返回
5.判断是那个文件描述符产生事件
if(FD_ISSET(0,&tempfds))
6.处理事件
{
fgets(buf,sizeof(buf),stdin);
}
if(FD_ISSET(sockfd,&tempfds))
{
acceptfd=accept();
}
select(检测文件描述符个数,读、写、异常,超时检测)
FD_SET:添加文件描述符到表中
FD_ZERO:清空表
FD_ISSET:判断对应文件描述符是否在表中
FD_CLR:从表中清除指定文件描述符
poll(表-结构体数组,数组有效元素的个数-检测文件描述符个数,-1->阻塞);
结构体:
fd
events:检测事件-POLLIN读 POLLOUT
revents:函数poll返回自动填充
如果对应fd有对应事件产生,将revents=events
如果对应fd没有对应事件产生,revents=0;
epoll:
int epfd=epoll_create(>0)
epoll_ctl(epfd,功能选择,fd,event-事件结构体)
功能选择:EPOLL_CTL_ADD 添加
EPOLL_CTL_MOD 修改已经添加事件
EPOLL_CTL_DEL 删除
event结构体:
data.fd
events: EPOLLIN|EPOLLET读
EPOLLOUT|EPOLLET 写
epoll_wait(epfd,事件存放的位置-事件结构体,数组元素个数,-1->阻塞);
多进程和多线程实现并发服务器思想:
每有一个客户端连接,创建一个子进程或线程和这个
客户端通信,父进程或主线程阻塞等待下一个客户端
连接。
fork创建进程的特点:
1.fork创建的子进程几乎拷贝了父进程所有的内容
三个段:正文、堆栈、数据段
2.fork之后父进程中返回子进程的PID,子进程中
返回0.
3.父进程先退出子进程孤儿进程,子进程先退出,
父进程没有回收资源,子进程僵尸进程。
4.fork之前的代码被复制,不会重新执行,fork之后
的代码会被复制并执行。
5.fork之前打开的文件,fork之后拿到的是同一个文件
描述符,操作同一个文件指针。
6.fork创建进程之后,两个进程就相互独立。
7.子进程状态发生改变会给父进程发送一个SIGCHLD信号
二、TCP编程
1.流程
服务器:
socket:创建一个用与链接的套接字
bind:绑定自己的ip地址和端口
listen:监听,将主动套接字转为被动套接字
accept:阻塞等待客户端链接,链接成功返回一个用于通信套接字
recv:接收消息
send:发送消息
close:关闭文件描述符
客户端:
socket:创建一个套接字
填充结构体:填充服务器的ip和端口
connect:阻塞等待链接服务器
recv/send:接收/发送消息
close:关闭
2.函数接口
1.socket
int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
domain:协议族
AF_UNIX, AF_LOCAL 本地通信
AF_INET ipv4
AF_INET6 ipv6
type:套接字类型
SOCK_STREAM:流式套接字
SOCK_DGRAM:数据报套接字
protocol:协议 - 填0 自动匹配底层 ,根据type
系统默认自动帮助匹配对应协议
传输层:IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP
网络层:htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL)
返回值:
成功 文件描述符
失败 -1,更新errno
2.bind
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:绑定 ipv4 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; };struct in_addr {uint32_t s_addr; //IP地址 }; 本地址通信结构体:struct sockaddr_un {
sa_family_t sun_family; //AF_UNIX char sun_path[108]; //在本地创建的套接字文件的路径及名字};ipv6通信结构体:
struct sockaddr_in6 {
sa_family_t sin6_family;
in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id;
};
struct in6_addr {unsigned char s6_addr[16];
};
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
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.connect
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:用于连接服务器;
参数:
sockfd:socket函数的返回值
addr:填充的结构体是服务器端的;
addrlen:结构体的大小
返回值 -1 失败,更新errno
正确 0
7.send
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送数据
参数:
sockfd:socket函数的返回值
buf:发送内容存放的地址
len:发送内存的长度
flags:如果填0,相当于write();
3.代码实现
优化代码
1.去掉fgets获取的多余的'\n'.if(buf[strlen(buf)-1] == '\n')//去掉fgets获取的'\n'
buf[strlen(buf)-1] ='\0';
2.端口和ip地址通过命令行传参到代码中。
3.设置客户端退出,服务器结束循环接收。
通过recv返回值为0判断客户端是否退出
4.设置来电显示功能,获取到请求链接服务器的客户端的ip和端口。
5.设置服务器端自动获取自己的ip地址。
INADDR_ANY "0.0.0.0"
6.实现循环服务器,服务器不退出,当链接服务器的客户端退出,服务器等到下一个客户端链接。
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>int main(int argc, char const *argv[])
{if (argc != 2){printf("please input %s <port>\n", argv[0]);return -1;}// 1.创建流式套接字socket .返回连接文件描述符int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr,caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));//saddr.sin_addr.s_addr = inet_addr(argv[1]);// saddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 0.0.0.0saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}while (1){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n",inet_ntoa(caddr.sin_addr),\ntohs(caddr.sin_port));//inet_ntoa// 5.收发消息char buf[64];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);}}// 6.关闭套接字close(acceptfd);}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>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 = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4 服务器struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[2]));//"8888"saddr.sin_addr.s_addr = inet_addr(argv[1]);if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}char buf[64];while(1){fgets(buf,sizeof(buf),stdin);//10 20//最多读size-1,自动补'\0',读到'\n'if(buf[strlen(buf)-1] == '\n')buf[strlen(buf)-1] = '\0';send(sockfd,buf,sizeof(buf),0);}// 6.关闭套接字close(sockfd);return 0;
}
4.tcp实现ftp功能
模拟FTP核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在,那么清空文件然后写入。
项目功能介绍:
均有服务器和客户端代码,基于TCP写的。
在同一路径下,将客户端可执行代码复制到其他的路径下,接下来再不同的路径下运行服务器和客户端。
相当于另外一台电脑在访问服务器。
客户端和服务器链接成功后出现以下提示:四个功能
***************list************** //列出服务器所在目录下的文件名(除目录不显示)
***********put filename********** //上传一个文件
***********get filename********** //重服务器所在路径下载文件
**************quit*************** //退出(可只退出客户端,服务器等待下一个客户端链接)
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 <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>void put_server(int acceptfd, char *buf, int size);
void list_server(int acceptfd, char *buf, int size);
int main(int argc, char const *argv[])
{if (argc != 2){printf("please input %s <port>\n", argv[0]);return -1;}// 1.创建流式套接字socket .返回连接文件描述符int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));//saddr.sin_addr.s_addr = inet_addr(argv[1]);// saddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 0.0.0.0saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}while (1){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port)); //inet_ntoa// 5.收发消息char buf[64];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);if (strncmp(buf, "list", 4) == 0){list_server(acceptfd, buf, sizeof(buf));}else if (strncmp(buf, "put ", 4) == 0){put_server(acceptfd, buf, sizeof(buf));}}}// 6.关闭套接字close(acceptfd);}close(sockfd);return 0;
}//1.list:打开当前目录,读目录中的文件判断文件是否是普通文件,
//是普通文件发送文件名给客户端
void list_server(int acceptfd, char *buf, int size)
{struct dirent *file = NULL;struct stat st;DIR *dir = opendir("./");if (NULL == dir){perror("opendir err.");return;}while ((file = readdir(dir)) != NULL){if (file->d_name[0] == '.')continue; //opendir(".")//获取文件属性,判断是普通文件发送给客户端stat(file->d_name, &st);if (S_ISREG(st.st_mode)){send(acceptfd, file->d_name, size, 0);}}//发送结束标志strcpy(buf, "send ok");send(acceptfd, buf, size, 0);
}//put:新建打开文件,接收写文件
void put_server(int acceptfd, char *buf, int size)
{int fd = open(buf + 4, O_WRONLY | O_CREAT | O_TRUNC, 0666) ;if (fd < 0){perror("open err.");return;}while (1){if (recv(acceptfd, buf, size, 0) < 0)//{perror("recv err.");return;}if (strncmp(buf, "send ok", 7) == 0)break;write(fd, buf, strlen(buf));//hello world\n//welcome\n//hi\n\0\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>void show(void);
void put_client(int sockfd,char *buf,int size);
void list_client(int sockfd,char *buf,int size);
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 = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4 服务器struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[2]));//"8888"saddr.sin_addr.s_addr = inet_addr(argv[1]);if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}char buf[64];while(1){show();fgets(buf,sizeof(buf),stdin);//10 20//最多读size-1,自动补'\0',读到'\n'if(buf[strlen(buf)-1] == '\n')buf[strlen(buf)-1] = '\0';send(sockfd,buf,sizeof(buf),0);if(strncmp(buf,"list",4)==0){//函数list_client(sockfd,buf,sizeof(buf));}else if(strncmp(buf,"put ",4)==0){//函数:打开本地文件,读文件内容发送给服务器put_client(sockfd,buf,sizeof(buf));}else if(strncmp(buf,"get ",4)==0){//函数:新建打开文件,接收写文件} }// 6.关闭套接字close(sockfd);return 0;
}void show(void)
{printf("--------------list------------------\n");printf("--------------put filename----------\n");printf("--------------get filename----------\n");printf("--------------quit------------------\n");
}//1.list:循环接收服务器发的文件名
void list_client(int sockfd,char *buf,int size)
{while(1){if(recv(sockfd,buf,size,0)<0){perror("recv err.");return ;}if(strncmp(buf,"send ok",7)==0){break;}printf("%s\n",buf);}
}//put:函数:打开本地文件,读文件内容发送给服务器
void put_client(int sockfd,char *buf,int size)
{//1.打开文件int fd=open(buf+4,O_RDONLY);//put test.cif(fd < 0){perror("open err.");return ;} //hello world\n//welcome\n//hi\nint ret;while( ret=read(fd,buf,size-1))//read size=10 hello wor\0 l d\nwelcome\n //hi\n\0\0\0\0\0\0\0{buf[ret]='\0';send(sockfd,buf,size,0);}strcpy(buf,"send ok");send(sockfd,buf,size,0);
}
三、UDP编程
1.通信流程
udp流程:(类似发短信)
server:
创建数据报套接字(socket(,SOCK_DGRAM,))----->有手机
绑定网络信息(bind())---------------------->绑定号码(发短信知道发给谁)
接收信息(recvfrom())--------------------->接收短信
关闭套接字(close())----------------------->接收完毕client:
创建数据报套接字(socket())----------------------->有手机
指定服务器的网络信息------------------------------>有对方号码
发送信息(sendto())---------------------------->发送短信
关闭套接字(close())--------------------------->发送完
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:发送端的网络信息结构体的大小的指针返回值:
成功接收的字节个数
失败:-10:客户端退出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
3.实现
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>int main(int argc, char const *argv[])
{//1.创建数据报套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//填充结构体struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);//2绑定if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}//3.循环收消息char buf[64];while (1){if (recvfrom(sockfd, buf, sizeof(buf), 0,(struct sockaddr *)&caddr, &len) < 0){perror("recv err.");return -1;}printf("%s %d:%s\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port), 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 <stdlib.h>
#include <string.h>
int main(int argc, char const *argv[])
{//1.创建数据报套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//填充结构体struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[2]));saddr.sin_addr.s_addr = inet_addr(argv[1]);socklen_t len = sizeof(caddr);//3.循环发消息char buf[64];while (1){fgets(buf,sizeof(buf),stdin);if(buf[strlen(buf)-1]=='\n')buf[strlen(buf)-1]='\0';sendto(sockfd,buf,sizeof(buf),0,\(struct sockaddr *)&saddr,sizeof(saddr));}close(sockfd);return 0;
}
4. 练习:实现如客户端发送"hello"给服务器端,服务器接着给客户端回,"recv:hello!!!!!"。
注意:
1、对于TCP是先运行服务器,客户端才能运行。
2、对于UDP来说,服务器和客户端运行顺序没有先后,因为是无连接,所以服务器和客户端谁先开始,没有关系,
3、一个服务器可以同时连接多个客户端。想知道是哪个客户端登录,可以在服务器代码里面打印IP和端口号。
4、UDP,客户端当使用send的时候,上面需要加connect,这个connect不是代表连接的作用,而是指定客户端即将要发送给谁数据。这样就不需要使用sendto而用send就可以。
5、在TCP里面,也可以使用recvfrom和sendto,使用的时候将后面的两个参数都写为NULL就OK。
#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>int main(int argc, char const *argv[])
{if (argc != 2){printf("please input %s <port>\n", argv[0]);return -1;}// 1.创建流式套接字socket .返回连接文件描述符int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr,caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));//saddr.sin_addr.s_addr = inet_addr(argv[1]);// saddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 0.0.0.0saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}while (1){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n",inet_ntoa(caddr.sin_addr),\ntohs(caddr.sin_port));//inet_ntoa// 5.收发消息char buf[64];int recvbyte;while (1){recvbyte = recv(acceptfd, buf, sizeof(buf), MSG_DONTWAIT);if (recvbyte < 0){perror("recv err.");// return -1;}else if (recvbyte == 0){printf("client exit.\n");break;}else{printf("buf:%s\n", buf);}}// 6.关闭套接字close(acceptfd);}close(sockfd);return 0;
}
5.项目-网络聊天室
5.1 项目要求
利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。
问题思考
- 客户端会不会知道其它客户端地址?
UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。
- 有几种消息类型?
- 登录:服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
- 聊天:服务器只需要把某个客户端的聊天消息转发给所有其它客户端。
- 退出:服务器删除退出客户端的地址,并把退出消息发送给其它客户端。
- 服务器如何存储客户端的地址?
数据结构可以选择线性数据结构
链表节点结构体:
struct node{struct sockaddr_in addr;//data memcmpstruct node *next;
};消息对应的结构体(同一个协议)
typedef struct msg_t
{int type;//'L' C Q enum un{login,chat,quit};char name[32];//用户名char text[128];//消息正文
}MSG_t;int memcmp(void *s1,void *s2,int size)
- 客户端如何同时处理发送和接收?
客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。
5.2 程序流程图
服务器端
客户端
客户端
server.c代码
#include <stdio.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>#include "head.h"
//链表节点结构体
typedef struct node_t
{struct sockaddr_in addr;struct node_t *next;
} link_t;void login_s(int sockfd, MSG_t msg, link_t *p, struct sockaddr_in caddr);
void quit_s(int sockfd, MSG_t msg, link_t *p, struct sockaddr_in caddr);
void chat_s(int sockfd, MSG_t msg, link_t *p, struct sockaddr_in caddr);
link_t *createLink(void);int main(int argc, char const *argv[])
{//1.创建数据报套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//填充服务器端结构体struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}MSG_t msg;pid_t pid = fork();if (pid > 0){//循环响应请求link_t *p = createLink();while (1){if (recvfrom(sockfd, &msg, sizeof(msg), 0,(struct sockaddr *)&caddr, &len) < 0){perror("recvfrom err.");return -1;}switch (msg.type){case Login:login_s(sockfd, msg, p, caddr);break;case Chat:chat_s(sockfd, msg, p, caddr);break;case Quit:quit_s(sockfd, msg, p, caddr);break;}}}else if (pid == 0){msg.type = Chat;strcpy(msg.name, "server");while (1){fgets(msg.text, sizeof(msg.text), stdin);if (msg.text[strlen(msg.text) - 1] == '\n')msg.text[strlen(msg.text) - 1] = '\0';sendto(sockfd, &msg, sizeof(msg), 0,(struct sockaddr *)&saddr, sizeof(saddr));}}return 0;
}
//0.创建一个空的有头单向链表
link_t *createLink(void)
{//1>malloc开辟节点空间link_t *p = (link_t *)malloc(sizeof(link_t));if (NULL == p){perror("malloc head node err.");return NULL;}p->next = NULL; //空return p;
}//1.登录函数
//功能:将谁登录发送给所有已经登录客户端
//将新登录客户端通信结构体插入链表
void login_s(int sockfd, MSG_t msg, link_t *p, struct sockaddr_in caddr)
{//1.遍历链表转发那个客户登录sprintf(msg.text, "%s login.", msg.name);while (p->next){p = p->next;sendto(sockfd, &msg, sizeof(msg), 0,(struct sockaddr *)&(p->addr), sizeof(p->addr));}//2.创建新节点保存新登录客户端的信息link_t *pnew = (link_t *)malloc(sizeof(link_t));if (NULL == pnew){perror("malloc new node err.");return;}//3.初始化节点pnew->addr = caddr;pnew->next = NULL;//4.链接到链表尾p->next = pnew;
}
//2.聊天
//功能:给所有除自己的客户端转发消息
void chat_s(int sockfd, MSG_t msg, link_t *p, struct sockaddr_in caddr)
{while (p->next != NULL){p = p->next;if (memcmp(&(p->addr), &caddr, sizeof(caddr)) != 0){sendto(sockfd, &msg, sizeof(msg), 0,(struct sockaddr *)&(p->addr), sizeof(p->addr));}}
}//3.退出
//功能:将谁退出转发给所有还登录着的客户,将自己的ip和端口从
//链表中删除。
void quit_s(int sockfd, MSG_t msg, link_t *p, struct sockaddr_in caddr)
{link_t *pdel = NULL;sprintf(msg.text, "%s quit", msg.name);while (p->next != NULL){if (memcmp(&(p->next->addr), &caddr, sizeof(caddr)) == 0){pdel = p->next;p->next = pdel->next;free(pdel);pdel = NULL;}else{p = p->next;sendto(sockfd, &msg, sizeof(msg), 0,(struct sockaddr *)&(p->addr), sizeof(p->addr));}}
}
client.c代码
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>#include "head.h"int main(int argc, char const *argv[])
{//1.创建数据报套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//填充服务器端结构体struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[2]));saddr.sin_addr.s_addr = inet_addr(argv[1]);MSG_t msg; //定义一个结构体变量保存要发送的消息//发送请求//1》登录:只登录一次msg.type = Login;printf("please input your name>>");fgets(msg.name, sizeof(msg.name), stdin);if (msg.name[strlen(msg.name) - 1] == '\n')msg.name[strlen(msg.name) - 1] = '\0';sendto(sockfd, &msg, sizeof(msg), 0,(struct sockaddr *)&saddr, sizeof(saddr));pid_t pid = fork();if (pid < 0){perror("fork err.");return -1;}else if (pid == 0){//循环聊天while (1){fgets(msg.text, sizeof(msg.text), stdin);if (msg.text[strlen(msg.text) - 1] == '\n')msg.text[strlen(msg.text) - 1] = '\0';if (strncmp(msg.text, "quit", 4) == 0){msg.type = Quit;sendto(sockfd, &msg, sizeof(msg), 0,(struct sockaddr *)&saddr, sizeof(saddr));break;}else{msg.type = Chat;sendto(sockfd, &msg, sizeof(msg), 0,(struct sockaddr *)&saddr, sizeof(saddr));}}}else{while (1){if (recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL) < 0){perror("recv from err.");return -1;}printf("%s:%s\n", msg.name, msg.text);}}close(sockfd);return 0;
}
四、linux下I/O模及特点
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
2. 非阻塞式IO
特点:可以处理多路IO;需要轮询,浪费CPU资源
•当我们将一个套接字设置为非阻塞模式,我们相当于告诉了系统内核:“当我请求的I/O 操作不能够马上完成,你想让我的进程进行休眠等待的时候,不要这么做,请马上返回一个错误给我。”
•当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停地测试是否一个文件描述符有数据可读(称做polling)。
•应用程序不停的polling 内核来检查是否I/O操作已经就绪。这将是一个极浪费CPU 资源的操作。
•这种模式使用中不普遍。
2.1 通过函数自带的参数设置非阻塞
2.2 通过设置文件描述符属性设置非阻塞(fcntl)
int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置文件描述符属性
参数:
fd:文件描述符
cmd:设置方式 - 功能选择
F_GETFL 获取文件描述符的状态信息 第三个参数化忽略
F_SETFL 设置文件描述符的状态信息 通过第三个参数设置
O_NONBLOCK 非阻塞
O_ASYNC 异步
O_SYNC 同步
arg:设置的值 in
返回值:
特殊选择返回特殊值 - F_GETFL 返回的状态值(int)
其他:成功0 失败-1,更新errno使用:0为例0-原本:阻塞、读权限 修改或添加非阻塞int flags=fcntl(0,F_GETFL);//1.获取文件描述符原有的属性信息
flags = flags | O_NONBLOCK;//2.修改添加权限fcntl(0,F_SETFL,flags); //3.将修改好的权限设置回去
#include
#include
#include
#include int main(int argc, char const *argv[])
{//设置0文件描述符的非阻塞//1.获取原属性int flags;
flags = fcntl(0, F_GETFL);//2.修改属性
flags = flags | O_NONBLOCK;//3.设置回去fcntl(0, F_SETFL, flags);char buf[32];while (1){sleep(1);if (fgets(buf, sizeof(buf), stdin) == NULL){perror("fgets err.");}printf("buf:%s\n", buf);}return 0;
}
3.信号驱动IO (异步IO模型 非重点)
特点:异步通知模式,需要底层驱动的支持
- 通过信号方式,当内核检测到设备数据后,会主动给应用发送信号SIGIO。
- 应用程序收到信号后做异步处理即可。
- 应用程序需要把自己的进程号告诉内核,并打开异步通知机制。
标准模板
//将APP进程号告诉驱动程序
fcntl(fd, F_SETOWN, getpid());//使能异步通知
int flag;
flag = fcntl(fd, F_GETFL);
flag |= O_ASYNC; //也可以用FASYNC标志
fcntl(fd, F_SETFL, flag);signal(SIGIO, handler);
示例:用非阻塞方式监听鼠标的数据(操作鼠标需要增加sudo权限
查看自己使用的鼠标:/dev/input
检查鼠标设备:sudo cat /dev/input/mouse0
#include
#include
#include
#include
#include int fd;
void handler(int sig)
{char buf[32] = "";int ret = read(fd, buf, sizeof(buf) - 1);
buf[ret] = '\0';printf("mouse:%s\n", buf);
}int main(int argc, char const *argv[])
{
fd = open("/dev/input/mouse0", O_RDONLY);if (fd < 0){perror("open mouse err.");return -1;}//1.将文件描述符、进程ID告诉底层驱动fcntl(fd, F_SETOWN, getpid());//2.设置fd文件描述符的异步通知属性int flags;
flags = fcntl(fd, F_GETFL);
flags |= O_ASYNC;fcntl(fd, F_SETFL, flags);//3.捕捉信号signal(SIGIO, handler);while (1){sleep(1);printf("hello world.\n");}return 0;
}
前三种使用场景假设总结:
假设妈妈有一个孩子,孩子在房间里睡觉,妈妈需要及时获知孩子是否醒了,如何做?
- 进到房间陪着孩子一起睡觉,孩子醒了会吵醒妈妈:不累,但是不能干别的了
- 时不时进房间看一下:简单,空闲时间还能干点别的,但是很累
- 妈妈在客厅干活,小孩醒了他会自己走出房门告诉妈妈:互不耽误
4. IO多路复用
4.1 IO多路复用场景假设
假设妈妈有三个孩子,分别不同的房间里睡觉,需要及时获知每个孩子是否醒了,如何做?
- 不停进每个房间看一下:简单,空闲时间还能干点别的,但是很累
- 把三个房间的门都打开,在客厅睡觉,同时监听所有房间的哭声,如果被哭声吵醒,那么能准确定位某个房间,及时处理即可:既能得到休息,也能及时获知每个孩子的状态。
4.2 IO多路复用机制
- 应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的;
- 若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间;
- 若设置多个进程/线程,分别处理一条数据通路,将新产生进程/线程间的同步与通信问题,使程序变得更加复杂;
- 比较好的方法是使用I/O多路复用技术。其基本思想是:
- 先构造一张有关描述符的表,然后调用一个函数。
- 当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
- 函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
基本流程:
1. 先构造一张有关文件描述符的表(集合、数组);
2. 将你关心的文件描述符加入到这个表中;
3. 然后调用一个函数。 select / poll
4. 当这些文件描述符中的一个或多个已准备好进行I/O操作的时候
该函数才返回(阻塞)。
5. 判断是哪一个或哪些文件描述符产生了事件(IO操作);
6. 做对应的逻辑处理;
5. 实现IO多路复用的方式
5.1 select
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);//清空表1
总结select实现IO多路复用特点*
1. 一个进程最多只能监听1024个文件描述符 (千级别)
2. select被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低(消耗CPU资源);
3. select每次会清空表,每次都需要拷贝用户空间的表到内核空间,效率低(一个进程0~4G,0~3G是用户态,3G~4G是内核态,拷贝是非常耗时的);
练习1:检测终端输入事件(键盘 0),鼠标输入事件。
//鼠标设备的路径:/dev/input/mouse0
#include
#include
#include
#include
#include
#include /* According to earlier standards */
#include
#include
#include int main(int argc, char const *argv[])
{//鼠标 /dev/input/mouse0//键盘:0 - stdinint fd_mouse = open("/dev/input/mouse0", O_RDONLY);if (fd_mouse < 0){perror("open mouse err.");return -1;}//引入IO多路复用机制 select 0 fd_mouse-检测读事件//1.创建表
fd_set readfds, tempfds;FD_ZERO(&readfds); //清空表//2.将关心文件描述符添加到表中FD_SET(0, &readfds);FD_SET(fd_mouse, &readfds);int maxfd = fd_mouse;char buf[32] = "";while (1){
tempfds = readfds;//3.调用select函数检测int ret = select(maxfd + 1, &tempfds, NULL, NULL, NULL);if (ret < 0){perror("select err.");return -1;}//4.当有一个或多个事件产生select函数返回//5.判断是那个或那几个产生事件if (FD_ISSET(0, &tempfds)){//6.处理事件//键盘fgets(buf, sizeof(buf), stdin);printf("key:%s\n", buf);}//鼠标if (FD_ISSET(fd_mouse, &tempfds)){int ret = read(fd_mouse, buf, sizeof(buf) - 1);
buf[ret] = '\0';printf("mouse:%s\n", buf);}}close(fd_mouse);return 0;
}
练习:尝试用select检测0 和sockfd (TCP),实现一个服务器响应多个客户端的连接,写完的提交群里一下。
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 <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{//1.创建套接字 socket TCPint sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1])); //"8888"saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);//2.绑定bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}//listen监听 将主动套接字变被动if (listen(sockfd, 5) < 0){perror("listen err.");return -1;}//引入select 0 sockfd - >读事件//1.创建表fd_set readfds, tempfds;FD_ZERO(&readfds);//2.添加关心文件描述符FD_SET(0, &readfds);FD_SET(sockfd, &readfds);int maxfd = sockfd;char buf[128];while (1){tempfds = readfds;int 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);for(int i=4;i<=maxfd;i++){if(FD_ISSET(i,&readfds))send(i,buf,sizeof(buf),0);}}if (FD_ISSET(sockfd, &tempfds)){int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){perror("accept err.");return -1;}printf("client: ip:%s port:%d\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));FD_SET(acceptfd, &readfds);if (maxfd < acceptfd)maxfd = acceptfd;}for (int i = 4; i <= maxfd; i++){if (FD_ISSET(i, &tempfds)){int ret=recv(i, buf, sizeof(buf), 0);if(ret < 0){perror("recv err.");return -1;}else if(ret == 0){printf("%d client exit.\n",i);FD_CLR(i,&readfds);//5close(i);if(maxfd==i)maxfd--;}else {printf("%d :%s\n",i,buf);}}}}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>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 = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4 服务器struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[2]));//"8888"saddr.sin_addr.s_addr = inet_addr(argv[1]);if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}char buf[64];pid_t pid=fork();if(pid < 0){perror("fork err");return -1;}else if(pid ==0){while(1){recv(sockfd,buf,sizeof(buf),0);printf("buf:%s\n",buf);}}else{while(1){fgets(buf,sizeof(buf),stdin);//10 20//最多读size-1,自动补'\0',读到'\n'if(buf[strlen(buf)-1] == '\n')buf[strlen(buf)-1] = '\0';send(sockfd,buf,sizeof(buf),0);}} // 6.关闭套接字close(sockfd);return 0;
}
5.2 poll实现
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
struct pollfd *fds
关心的文件描述符数组struct pollfd fds[N];
nfds:个数
timeout: 超时检测
毫秒级的:如果填1000,1秒
如果-1,阻塞
struct pollfd {
int fd; /* 检测的文件描述符 */
short events; /* 检测事件 */
short revents; /* 调用poll函数返回填充的事件,poll函数一旦返回,将对应事件自动填充结构体这个成员。只需要判断这个成员的值就可以确定是否产生事件 */
};
事件: POLLIN :读事件
POLLOUT : 写事件
POLLERR:异常事件
poll实现IO多路复用的特点
1. 优化文件描述符个数的限制;(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组元素个数为1,如果想监听100个,那么这个结构体数组的元素个数就为100,由程序员自己来决定)
2. poll被唤醒之后需要重新轮询一遍驱动的poll函数,效率比较低
3. poll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可
实现代码
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include int main(int argc, char const *argv[])
{if (argc != 2){printf("please input %s \n", argv[0]);return -1;}// 1.创建流式套接字socket .返回连接文件描述符int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); socklen_t len = sizeof(caddr);// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}//poll:0 sockfd acceptfd ->读事件//1.创建表struct pollfd fds[200] = {};//2.将关心文件描述符添加到表中
fds[0].fd = 0;
fds[0].events = POLLIN; fds[1].fd = sockfd;
fds[1].events = POLLIN;int last = 1;char buf[64];while (1){//调用poll监测int ret = poll(fds, last + 1, -1); //阻塞if (ret < 0){perror("poll err.");return -1;}for (int i = 0; i <= last; 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;i<=last;j++){send(fds[j].fd,buf,sizeof(buf),0);}}else if (fds[i].fd == sockfd){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port)); //inet_ntoa//将acceptfd添加到表
last++;
fds[last].fd = acceptfd;
fds[last].events = POLLIN;}else{int 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[last];
last--;
i--;}else{printf("%d :%s\n", fds[i].fd, buf);}}}}}close(sockfd);return 0;
}
5.3 epoll实现 (异步)
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中删除一个fdFd:需要监听的fd。event:告诉内核需要监听什么事件EPOLLIN:表示对应文件描述符可读EPOLLOUT:可写EPOLLPRI:有紧急数据可读;EPOLLERR:错误;EPOLLHUP:被挂断;EPOLLET:触发方式,边缘触发;(默认使用边缘触发)ET模式:表示状态的变化;
返回值:成功时返回0,失败时返回-1typedef 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不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.
#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/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 = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}//epoll:0 sockfd acceptfd ->读事件struct epoll_event event; //暂时保存添加的事件struct epoll_event revent[10]; //暂时保存从链表中拿出来的事件//1.创建树int epfd = epoll_create(1);//2.将关心文件描述符添加到数上event.data.fd = 0;event.events = EPOLLIN | EPOLLET;epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);event.data.fd = sockfd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);int last = 1;char buf[64];while (1){//拿事件处理,ret实际拿到的个数int ret = epoll_wait(epfd, revent, 10, -1);if (ret < 0){perror("epoll err.");return -1;} for (int i = 0; i < ret; i++){if (revent[i].data.fd == 0){fgets(buf, sizeof(buf), stdin);printf("key:%s\n", buf);}else if (revent[i].data.fd == sockfd){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port)); //inet_ntoa//将acceptfd添加到表树上event.data.fd=acceptfd;event.events=EPOLLIN|EPOLLET;epoll_ctl(epfd,EPOLL_CTL_ADD,acceptfd,&event); }else{int recvbyte = recv(revent[i].data.fd, buf, sizeof(buf), 0);if (recvbyte < 0){perror("recv err.");return -1;}else if (recvbyte == 0){printf("%d client exit\n", revent[i].data.fd);close(revent[i].data.fd);epoll_ctl(epfd,EPOLL_CTL_DEL,revent[i].data.fd,NULL); }else{printf("%d :%s\n",revent[i].data.fd, buf);}}}}close(sockfd);return 0;
}
五、服务器模型
- 在网络程序里面,通常都是一个服务器处理多个客户机。
- 为了处理多个客户机的请求, 服务器端的程序有不同的处理方式。
1.循环服务器模型
同一个时刻只能响应一个客户端的请求,伪代码如下:
socket()
bind();
listen();
while(1)
{accept();while(1){process(); //处理}close();
}
2. 并发服务器模型
同一个时刻可以响应多个客户端的请求,常用的模型有多进程模型/多线程模型/IO多路复用模型。
多进程和多线程实现并发服务器思想:
每有一个客户端连接,创建一个子进程或线程和这个客户端通信,父进程或主线程阻塞等待下一个客户端连接。
2.1 多进程模型
每来一个客户端连接,开一个子进程来专门处理客户端的数据,实现简单,但是系统开销相对较大,更推荐使用线程模型。伪代码如下:
socket()
bind();
listen();
while(1)
{accept(); if(fork() == 0) //子进程{while(1){process();}close(client_fd);exit();}
}
注意:收到客户端消息后,打印下是来自哪个客户端的数据(来电显示)
使用SIGCHLD来处理子进程结束的信号,信号函数中回收进程资源。
fork.c代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main(int argc, char const *argv[])
{int a=100;char buf[32];int ret;printf("88888888888888888888888\n");int fd=open("./fork.c",O_RDONLY);pid_t pid=fork();if(pid < 0){perror("fork err.");return -1;}else if(pid == 0){a=10000;printf("my child. %d %d\n",a,fd);ret=read(fd,buf,sizeof(buf)-1);buf[ret]='\0';printf("buf:%s\n",buf);close(fd);}else{sleep(1);printf("my father %d %d\n",a,fd);ret=read(fd,buf,sizeof(buf)-1);buf[ret]='\0';printf("buf:%s\n",buf);}printf("------------------------%d\n",a);return 0;
}
fork创建进程的特点:
1.fork创建的子进程几乎拷贝了父进程所有的内容
三个段:正文、堆栈、数据段
2.fork之后父进程中返回子进程的PID,子进程中
返回0.
3.父进程先退出子进程孤儿进程,子进程先退出,
父进程没有回收资源,子进程僵尸进程。
4.fork之前的代码被复制,不会重新执行,fork之后
的代码会被复制并执行。
5.fork之前打开的文件,fork之后拿到的是同一个文件
描述符,操作同一个文件指针。
6.fork创建进程之后,两个进程就相互独立。
7.子进程状态发生改变会给父进程发送一个SIGCHLD信号
pthread_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/wait.h>
#include <signal.h>void handler(int sig)
{waitpid(-1, NULL, WNOHANG);
}int main(int argc, char const *argv[])
{if (argc != 2){printf("please input %s <port>\n", argv[0]);return -1;}// 1.创建流式套接字socket .返回连接文件描述符int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}//注册信号signal(SIGCHLD, handler); //void (*handler)(int )while (1){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port)); //inet_ntoapid_t pid = fork();if (pid < 0){perror("fork err.");return -1;}else if (pid == 0){close(sockfd);// 5.收发消息char buf[64];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);}}close(acceptfd);exit(-1); //结束子进程}// 6.关闭套接字close(acceptfd);}close(sockfd);return 0;
}
2.2 多线程模型
每来一个客户端连接,开一个子线程来专门处理客户端的数据,实现简单,占用资源较少,属于使用比较广泛的模型:
socket()
bind();
listen();
while(1)
{accept();pthread_create();
}
signal.c 代码
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>void handler(int sig)
{printf("--------------ctrt+c\n");
}int main(int argc, const char *argv[])
{signal(SIGINT,handler);while(1){sleep(5);printf("hello world.\n");}return 0;
}
pthread_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/wait.h>
#include <signal.h>
#include <pthread.h> //void *(*thread)(void *)
void *pthread(void *arg)
{int acceptfd = *((int *)arg);char buf[64];int recvbyte;while (1){recvbyte = recv(acceptfd, buf, sizeof(buf), 0);if (recvbyte < 0){perror("recv err.");return NULL;}else if (recvbyte == 0){printf("client exit.\n");break;}else{printf("%d buf:%s\n", acceptfd, buf);}}close(acceptfd);return NULL; //pthread_exit(NULL);
}int main(int argc, char const *argv[])
{if (argc != 2){printf("please input %s <port>\n", argv[0]);return -1;}// 1.创建流式套接字socket .返回连接文件描述符int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}while (1){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port)); //inet_ntoapthread_t tid;pthread_create(&tid, NULL, pthread, &acceptfd);pthread_detach(tid);}close(sockfd);return 0;
}
2.3 IO多路复用模型
借助select、poll、epoll机制,将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接,在嵌入式开发中应用广泛,不过代码写起了稍显繁琐。
3. 网络超时检测
3.1 应用场景
- 在网络通信中,很多操作会使得进程阻塞:
- TCP套接字中的recv/accept
- UDP套接字中的recvfrom
- 超时检测的必要性
- 避免进程在没有数据时无限制地阻塞
- 实现某些特定协议要求,比如某些设备规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,需要做出一些特殊处理
3.2 利用函数参数设置
如使用select/poll/epoll函数最后一个参数可以设置超时。
1.select设置超时
struct timeval tm = {2, 0};//设置2s打算阻塞
sret = select(maxfd + 1, &tempfds, NULL, NULL, &tm);
第五个参数:struct timeval {long tv_sec; /*秒*/long tv_usec; /*微秒*/};2.pollint poll(struct pollfd *fds, nfds_t nfds, int timeout);
第三个参数:时间单位是毫秒 -1阻塞, 2000=2s
ret = poll(event, num, 2000);//超时检测时间为2s3.epoll 设置的是epoll_waitint epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
第四个参数:时间单位是毫秒 -1阻塞, 2000=2s
ret = epoll_wait(epfd, events, 20, 2000);设置超时后的返回值都为:<0 error=0 超时>0 正确
select.c代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.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[])
{//鼠标 /dev/input/mouse0//键盘:0 - stdinint fd_mouse = open("/dev/input/mouse0", O_RDONLY);if (fd_mouse < 0){perror("open mouse err.");return -1;}//引入IO多路复用机制 select 0 fd_mouse-检测读事件//1.创建表fd_set readfds, tempfds;FD_ZERO(&readfds); //清空表//2.将关心文件描述符添加到表中FD_SET(0, &readfds);FD_SET(fd_mouse, &readfds);int maxfd = fd_mouse;char buf[32] = "";while (1){tempfds = readfds;//3.调用select函数检测struct timeval tv={2,0};int ret = select(maxfd + 1, &tempfds, NULL, NULL, &tv);if (ret < 0){perror("select err.");return -1;}else if(ret == 0){printf("time out --------------------\n");continue;}//4.当有一个或多个事件产生select函数返回//5.判断是那个或那几个产生事件if (FD_ISSET(0, &tempfds)){//6.处理事件//键盘fgets(buf, sizeof(buf), stdin);printf("key:%s\n", buf);}//鼠标if (FD_ISSET(fd_mouse, &tempfds)){int ret = read(fd_mouse, buf, sizeof(buf) - 1);buf[ret] = '\0';printf("mouse:%s\n", buf);}}close(fd_mouse);return 0;
}
poll.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 <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 = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}//poll:0 sockfd acceptfd ->读事件//1.创建表struct pollfd fds[200] = {};//2.将关心文件描述符添加到表中fds[0].fd = 0;fds[0].events = POLLIN;fds[1].fd = sockfd;fds[1].events = POLLIN;int last = 1;char buf[64];while (1){//调用poll监测int ret = poll(fds, last + 1,2000); //阻塞if (ret < 0){perror("poll err.");return -1;}//当事件产生,将这个结构体元素中第二个成员的值赋值给第三个成员。//没有事件第三个成员为0else if(ret == 0){printf("time out -----------------\n");continue; }for (int i = 0; i <= last; 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<=last;j++){send(fds[j].fd,buf,sizeof(buf),0);}}else if (fds[i].fd == sockfd){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port)); //inet_ntoa//将acceptfd添加到表last++;fds[last].fd = acceptfd;fds[last].events = POLLIN;}else{int 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[last];last--;i--;}else{printf("%d :%s\n", fds[i].fd, buf);}}}}}close(sockfd);return 0;
}
epoll.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/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 = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}//epoll:0 sockfd acceptfd ->读事件struct epoll_event event; //暂时保存添加的事件struct epoll_event revent[10]; //暂时保存从链表中拿出来的事件//1.创建树int epfd = epoll_create(1);//2.将关心文件描述符添加到数上event.data.fd = 0;event.events = EPOLLIN | EPOLLET;epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);event.data.fd = sockfd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);char buf[64];while (1){//拿事件处理,ret实际拿到的个数int ret = epoll_wait(epfd, revent, 10, 2000);if (ret < 0){perror("epoll err.");return -1;}else if(ret == 0){printf("timeout -----------------\n");continue;}for (int i = 0; i < ret; i++){if (revent[i].data.fd == 0){fgets(buf, sizeof(buf), stdin);printf("key:%s\n", buf);}else if (revent[i].data.fd == sockfd){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port)); //inet_ntoa//将acceptfd添加到表树上event.data.fd = acceptfd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &event);}else{int recvbyte = recv(revent[i].data.fd, buf, sizeof(buf), 0);if (recvbyte < 0){perror("recv err.");return -1;}else if (recvbyte == 0){printf("%d client exit\n", revent[i].data.fd);close(revent[i].data.fd);epoll_ctl(epfd, EPOLL_CTL_DEL, revent[i].data.fd, NULL);}else{printf("%d :%s\n", revent[i].data.fd, buf);}}}}close(sockfd);return 0;
}
3.3 利用setsockopt属性设置
Linux中socket属性
选项名称 说明 数据类型
========================================================================
SOL_SOCKET 应用层
------------------------------------------------------------------------
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 允许调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获得套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSEADDR 允许重用本地地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int
==========================================================================
IPPROTO_IP IP层/网络层
----------------------------------------------------------------------------
IP_HDRINCL 在数据包中包含IP首部 int
IP_OPTINOS IP首部选项 int
IP_TOS 服务类型
IP_TTL 生存时间 int
IP_ADD_MEMBERSHIP 将指定的IP加入多播组 struct ip_mreq
==========================================================================
IPPRO_TCP 传输层
-----------------------------------------------------------------------------
TCP_MAXSEG TCP最大数据段的大小 int
TCP_NODELAY 不使用Nagle算法 int
API接口
int getsockopt(int sockfd,int level,int optname,void *optval,socklen_t *optlen)
int setsockopt(int sockfd,int level,int optname,void *optval,socklen_t optlen)功能:获得/设置套接字属性
参数:
sockfd:套接字描述符
level:协议层
SOL_SOCKET(应用层)
IPPROTO_TCP(传输层)
IPPROTO_IP(网络层)
optname:选项名
SO_BROADCAST 允许发送广播数据 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval optval:选项值
optlen:选项值大小指针
#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>int main(int argc, char const *argv[])
{if (argc != 2){printf("please input %s <port>\n", argv[0]);return -1;}// 1.创建流式套接字socket .返回连接文件描述符int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//ipv4struct sockaddr_in saddr,caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);//设置端口重用int optval=1;setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval));// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}while (1){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd,(struct sockaddr *)&caddr,&len);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);printf("client:ip=%s port=%d\n",inet_ntoa(caddr.sin_addr),\ntohs(caddr.sin_port));//inet_ntoa// 5.收发消息char buf[64];int recvbyte;while (1){//设置接收超时struct timeval tv={2,0};setsockopt(acceptfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv));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);}}// 6.关闭套接字close(acceptfd);}close(sockfd);return 0;
}
设置超时检测操作
struct timeval {long tv_sec; /*秒*/long tv_usec; /*微秒*/};
//设置接收超时struct timeval tm={2,0}; setsockopt(acceptfd,SOL_SOCKET,SO_RCVTIMEO,&tm,sizeof(tm));//设置超时之后时间到打断接下来的阻塞在这个文件描述符的函数,直接错误返回补充:
//设置端口和地址重用
int optval=1;
setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&optval,sizeof(optval));
3.4 alarm定时器设置
alarm(5) 闹钟 定时器
//5秒之后会,会有一个信号产生(SIGALRM)
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
功能:对接收到的指定信号处理
signum 信号 struct sigaction {void (*sa_handler)(int);};//设置信号属性struct sigaction act;sigaction(SIGALRM,NULL,&act);//获取原属性
act.sa_handler=handler;//修改属性sigaction(SIGALRM,&act,NULL);//将修改的属性设置回去注:在recv前调用alarm函数
alarm的 SIGALRM信号产生后会打断(终端)下面的系统调用recv;
打断后相当于recv错误返回。
#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("time out -----------------\n");
}int main(int argc, char const *argv[])
{//SIGALRMstruct sigaction act;//1.获取原属性sigaction(SIGALRM,NULL,&act);//2.修改act.sa_handler=handler;//3.设置sigaction(SIGALRM,&act,NULL);char buf[32];while(1){alarm(2);if(fgets(buf,sizeof(buf),stdin)== NULL){perror("fgets err.");}printf("buf:%s\n",buf);}return 0;
}
六、广播、组播、本地套接字通信
1. 广播
1.1 理论
- 前面介绍的数据包发送方式只有一个接受方,称为单播
- 如果同时发给局域网中的所有主机,称为广播
- 只有用户数据报(使用UDP协议)套接字才能广播
- 一般被设计成局域网搜索协议
- 广播地址
- 以192.168.1.0 (255.255.255.0) 网段为例,最大的主机地址192.168.1.255代表该网段的广播地址
- 发到该地址的数据包被所有的主机接收
1.2 广播发送流程
- 创建用户数据报套接字
- 缺省创建的套接字不允许广播数据包,需要设置属性(setsockopt)
- 接收方地址指定为广播地址
- 指定端口信息
- 发送数据包
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>int main(int argc, char const *argv[])
{//1.创建数据报套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//2.设置发送广播属性int optval = 1;setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &optval, sizeof(optval));//3.填充广播IP和端口struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[2]));saddr.sin_addr.s_addr = inet_addr(argv[1]);char buf[128];//4.发送广播消息while (1){fgets(buf, sizeof(buf), stdin);if (buf[strlen(buf) - 1] == '\n')buf[strlen(buf) - 1] = '\0';sendto(sockfd, buf, sizeof(buf), 0,(struct sockaddr *)&saddr, sizeof(saddr));}close(sockfd);return 0;
}
1.3 广播接收 流程
- 创建用户数据报套接字
- 绑定IP地址(广播IP或0.0.0.0)和端口
- 绑定的端口必须和发送方指定的端口相同
- 等待接收数据
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>int main(int argc, char const *argv[])
{//1.创建数据报套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//2.填充广播IP和端口struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1]));saddr.sin_addr.s_addr = inet_addr("0.0.0.0");socklen_t len = sizeof(caddr);//3.绑定if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}char buf[128];//4.收广播消息while (1){if (recvfrom(sockfd, buf, sizeof(buf), 0,(struct sockaddr *)&caddr, &len) < 0){perror("recvfrom err.");return -1;}printf("ip=%s port=%d : %s\n", inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port), buf);}close(sockfd);return 0;
}
2. 组播
2.1 理论
- 单播方式只能发给一个接收方。
- 广播方式发给所有的主机。过多的广播会大量占用网络带宽,造成广播风暴,影响正常的通信。
- 组播是一个人发送,加入到多播组的人接收数据。
- 多播方式既可以发给多个主机,又能避免象广播那样带来过多的负载(每台主机要到传输层才能判断广播包是否要处理)
2.2 组播地址
不分网络地址和主机地址,第1字节的前4位固定为1110 。是D类IP
224.0.0.1 – 239.255.255.255
2.3 组播发送
- 创建用户数据报套接字
- 接收方地址指定为组播地址
- 指定端口信息
- 发送数据包
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h>int main(int argc, char const *argv[]) {int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//填充结构体 组ipstruct sockaddr_in gaddr;gaddr.sin_family = AF_INET;gaddr.sin_port = htons(atoi(argv[2]));gaddr.sin_addr.s_addr = inet_addr(argv[1]); //组ipchar buf[128];while (1){fgets(buf, sizeof(buf), stdin);sendto(sockfd,buf,sizeof(buf),0,\(struct sockaddr *)&gaddr,sizeof(gaddr));}close(sockfd);return 0; }
2.4 组播接收
- 创建用户数据报套接字
- 加入多播组
- 绑定IP地址(加入组的组IP或0.0.0.0)和端口
- 等待接收数据
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>int main(int argc, char const *argv[])
{int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){perror("socket err.");return -1;}//将ip地址加入多播组struct ip_mreq mreq;mreq.imr_multiaddr.s_addr=inet_addr(argv[1]); mreq.imr_interface.s_addr=inet_addr("0.0.0.0"); setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq));//填充结构体 组ipstruct sockaddr_in gaddr;gaddr.sin_family = AF_INET;gaddr.sin_port = htons(atoi(argv[2]));gaddr.sin_addr.s_addr = inet_addr(argv[1]); //组ip//绑定if(bind(sockfd,(struct sockaddr *)&gaddr,sizeof(gaddr))<0){perror("bind err.");return -1;}char buf[128];while (1){recvfrom(sockfd,buf,sizeof(buf),0,NULL,NULL);printf("buf:%s\n",buf);}close(sockfd);return 0;
}
加入多播组核心代码:
struct ip_mreq
{struct in_addr imr_multiaddr; /* 指定多播组IP */struct in_addr imr_interface; /* 本地网卡地址,通常指定为 INADDR_ANY--0.0.0.0*/};
}
struct ip_mreq mreq;
bzero(&mreq, sizeof(mreq));
mreq.imr_multiaddr.s_addr = inet_addr("224.10.10.1");
mreq.imr_interface.s_addr = INADDR_ANY;
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
3. 本地套接字通信
3.1 特性
- socket同样可以用于本地通信
- 创建套接字时使用本地协议AF_UNIX(或AF_LOCAL)。
- 分为流式套接字和用户数据报套接字
- 和其他进程间通信方式相比使用方便、效率更高
- 常用于前后台进程通信
3.2 核心代码
#include
#include unix_socket = socket(AF_UNIX, type, 0);struct sockaddr_un {sa_family_t sun_family; /* AF_UNIX */char sun_path[UNIX_PATH_MAX]; /* 本地路径 */
};struct sockaddr_un myaddr;
bzero(&myaddr, sizeof(myaddr));
myaddr.sun_family = AF_UNIX;
strcpy(myaddr.sun_path, "mysocket"); //可以指定路径
3.3 代码实现
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/socket.h>
#include <sys/un.h>
#include <string.h>int main(int argc, char const *argv[])
{// 1.创建流式套接字socket .返回连接文件描述符int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}struct sockaddr_un saddr;saddr.sun_family = AF_UNIX;strcpy(saddr.sun_path, "./myunix");// system("rm ./myunix -f");unlink("./myunix");// 2.绑定(填充通信结构体)bindif (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}// 3.监听。主动套接字变为被动套接字listenif (listen(sockfd, 5) < 0){perror("listen err.");return -1;}while (1){// 4.阻塞等待客户端连接accept .返回通信文件描述符int acceptfd = accept(sockfd, NULL, NULL);if (acceptfd < 0){perror("accept err.");return -1;}printf("sockfd:%d acceptfd:%d\n", sockfd, acceptfd);// 5.收发消息char buf[64];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);}}// 6.关闭套接字close(acceptfd);}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 <sys/socket.h>
#include <sys/un.h>int main(int argc, char const *argv[])
{// 1.创建流式套接字socket .返回连接文件描述符int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err.");return -1;}struct sockaddr_un saddr;saddr.sun_family = AF_UNIX;strcpy(saddr.sun_path, "./myunix");if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("bind err.");return -1;}char buf[64];while (1){fgets(buf, sizeof(buf), stdin); //10 20//最多读size-1,自动补'\0',读到'\n'if (buf[strlen(buf) - 1] == '\n')buf[strlen(buf) - 1] = '\0';send(sockfd, buf, sizeof(buf), 0);}// 6.关闭套接字close(sockfd);return 0;
}
七、网络协议头分析(了解)
1. 数据的封装与传递过程
思考:
- 应用层调用send后,是如何把数据发送到另一台机器的某个进程的。
- 接收的设备收到数据包后,如何处理给应用层?
思考:在协议栈封装的过程中,这些头部信息具体有什么呢?
2. 以太网帧完整帧格式
- 对于网络层最大数据帧长度是1500字节
- 对于链路层最大数据长度是1518字节(1500+14+CRC)
- 发送时候,IP层协议栈程序检测到发送数据和包头总长度超过1500字节时候,会进行自动分包处理,接收端在IP层进行包重组,然后才继续往上传递
粘包、拆包发生原因: 1000 - 800+200 400 200
发生TCP粘包或拆包有很多原因,常见的几点: 600 1000 400
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(最大报文长度),TCP在传输前将进行拆包。
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。粘包、拆包解决办法:
解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。 等等。
4.延时、效率低
tcp粘包与udp丢包的原因及解决:
https://www.cnblogs.com/111testing/p/12810253.html
3. 以太网头部
4. IP头
【腾讯文档】IP数据包的格式
IP数据包的格式
5. TCP头
6. UDP头
【腾讯文档】TCP数据包格式
TCP数据包格式
7. 三次握手和四次挥手(TCP*)
7.1 三次握手
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
服务器必须准备好接受外来的连接。这通过调用socket、 bind和listen函数来完成,称为被动打开(passive open)。
第一次握手:客户通过调用connect进行主动打开(active open)。这引起客户TCP发送一个SYN(表示同步)分节(SYN=J),它告诉服务器客户将在连接中发送到数据的初始序列号。并进入SYN_SEND状态,等待服务器的确认。
第二次握手:服务器必须确认客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器以单个字节向客户发送SYN和对客户SYN的ACK(表示确认),此时服务器进入SYN_RECV状态。
第三次握手:客户收到服务器的SYN+ACK。向服务器发送确认分节,此分节发送完毕,客户服务器进入ESTABLISHED状态,完成三次握手。
辅助了解:
客户端的初始序列号为J,而服务器的初始序列号为K。在ACK里的确认号为发送这个ACK的一端所期待的下一个序列号。因为SYN只占一个字节的序列号空间,所以每一个SYN的ACK中的确认号都是相应的初始序列号加1.类似地,每一个FIN(表示结束)的ACK中的确认号为FIN的序列号加1.
完成三次握手,客户端与服务器开始传送数据,在上述过程中还有一些重要概念。
未连接队列:在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户端确认包。这些条目所标识的连接在服务器处于SYN_RECV状态,当服务器收到客户端确认包时,删除该条目,服务器进入ESTABLISHED状态。
7.2 四次挥手
TCP连接终止需四个分节。
第二次握手:接收到FIN的另一端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为文件结束符传递给接收端应用进程(放在已排队等候应用进程接收到任何其他数据之后)
第三次握手:一段时间后,接收到文件结束符的应用进程将调用close关闭它的套接口。这导致它的TCP也发送一个FIN。
第四次握手:接收到这个FIN的原发送端TCP对它进行确认。
第一次握手:某个应用进程首先调用close,我们称这一端执行主动关闭。这一端的TCP于是发送一个FIN分节,表示数据发送完毕。
【腾讯文档】TCP握手挥手的过程分析
TCP握手挥手的过程分析
8. wireshark抓包工具
简单使用过程
1. 安装 sudo apt-get update
sudo apt-get install wireshark
2. 运行
sudo wireshark
3. 过滤
tcp.port == 8888
4. 抓的是流经eth0网卡的数据
服务器端代码运行在ubuntu
客户端代码运行在windows下 ip.addr == 192.168.1.31注:抓包的过程,就是抓网卡流经的一些数据。启动时不加sudo找不到网卡,没有办法找到内容。
如何抓包:1.启动wireshark 。//filter-》过滤器
2.想抓流经eth0网卡的数据,就点击一下eth0.
3.想找到我想抓的数据,需要用到filter
//在这之前,需要将ubuntu的ip修改固定ip和windows在同一网段。
4.在filter,输入:tcp port==8888,回车查找端口号为8888的流经的数据。
//通过端口号进行的过滤,也可通过ip(通过Expression按键可以查看过滤方式)
八、数据库编程
1. 数据库简介
常用的数据库
大型数据库 :Oracle中型数据库 :Server是微软开发的数据库产品,主要支持windows平台
小型数据库 : mySQL是一个小型关系型数据库管理系统。开放源码 SQLite基础
SQLite的源代码是C,其源代码完全开放。它是一个轻量级的嵌入式数据库。
SQLite有以下特性:
零配置一无需安装和管理配置;
储存在单一磁盘文件中的一个完整的数据库;
数据库文件可以在不同字节顺序的机器间自由共享;
支持数据库大小至2TB(1024G = 1TB);足够小,全部源码大致3万行c代码,250KB;
比目前流行的大多数数据库对数据的操作要快;创建SQLite数据库:
手工创建
使用sqlite3工具,通过手工输入SQL命令行完成数据库创建.
用户在Linux的命令行界面中输入sqlite3可启动sqlite3工具
代码创建
在代码中常动态创建数据库 在程序运行过程中,当需要进行数据库操作时,应用程序会首先尝试打开数据库,此时如果数据库并不存在,程序则会自动建立数据库,然后再打开数据库
2. 虚拟中sqlite3安装
sqlite3 安装:
1. sudo dpkg -i *.deb 离线安装
2. 在线安装
1、设置能够上网
2、更新更新源#apt-get update 3、安装软件及开发环境# apt-get install sqlite3 --->sqlite3数据库软件# apt-get install libsqlite3-dev --->sqlite3数据库开发支持库# apt-get install sqlite3-doc --->sqlite3数据库说明文档-------------------------------- #apt-get install sqlitebrowser --->sqlite3数据库操作软件
源码安装:
tar xf sqlite-autoconf-3140100.tar.gz
./configure
make
sudo make install
安装完成后,可以使用sqlite3 -version命令来测试是否安装成功
$ sqlite3 -version
3.14.1 2016-08-11
3. 基础SQL语句使用
【腾讯文档】sqlite基础SQL语句使用
sqlite基础SQL语句使用
4. sqlite使用入门
数据库 · 华清远见教学空间
5. sqlite3编程
API接口文档
官方文档:List Of SQLite Functions
中文文档:SQLite 命令 - SQLite 中文版 - UDN开源文档
头文件:#include <sqlite3.h>
编译:gcc sqlite1.c -lsqlite31.int sqlite3_open(char *path, sqlite3 **db);功能:打开sqlite数据库,如果数据库不存在则创建它
path: 数据库文件路径
db: 指向sqlite句柄的指针
返回值:成功返回SQLITE_OK,失败返回错误码(非零值)2.int sqlite3_close(sqlite3 *db);功能:关闭sqlite数据库
返回值:成功返回SQLITE_OK,失败返回错误码返回值:返回错误信息3.执行sql语句接口
int sqlite3_exec(
sqlite3 *db, /* An open database */const char *sql, /* SQL to be evaluated */int (*callback)(void*,int,char**,char**), /* Callback function */void *arg, /* 1st argument to callback */char **errmsg /* Error msg written here */
);功能:执行SQL操作
db:数据库句柄
sql:要执行SQL语句
callback:回调函数(满足一次条件,调用一次函数,用于查询)
再调用查询sql语句的时候使用回调函数打印查询到的数据
arg:传递给回调函数的参数
errmsg:错误信息指针的地址
返回值:成功返回SQLITE_OK,失败返回错误码回调函数:
typedef int (*sqlite3_callback)(void *para, int f_num, char **f_value, char **f_name);功能:select:每找到一条记录自动执行一次回调函数
para:传递给回调函数的参数(由 sqlite3_exec() 的第四个参数传递而来)
f_num:记录中包含的字段数目
f_value:包含每个字段值的指针数组(列值)
f_name:包含每个字段名称的指针数组(列名)
返回值:成功返回SQLITE_OK,失败返回-1,每次回调必须返回0后才能继续下次回调4.不使用回调函数执行SQL语句(只用于查询)
int sqlite3_get_table(sqlite3 *db, const char *sql, char ***resultp, int *nrow, int *ncolumn, char **errmsg);功能:执行SQL操作
db:数据库句柄
sql:SQL语句
resultp:用来指向sql执行结果的指针
nrow:满足条件的记录的数目(但是不包含字段名(表头 id name score))
ncolumn:每条记录包含的字段数目
errmsg:错误信息指针的地址返回值:成功返回SQLITE_OK,失败返回错误码5.返回sqlite3定义的错误信息
char *sqlite3_errmsg(sqlite3 *db);
#include <stdio.h>
#include <sqlite3.h>int callback(void *arg, int f_num, char **f_value, char **f_name)
{printf("%s\n", (char *)arg);for (int i = 0; i < f_num; i++){printf("%s ", f_name[i]);}putchar(10);for (int i = 0; i < f_num; i++){printf("%s ", f_value[i]);}putchar(10);return 0;
}
int main(int argc, char const *argv[])
{//1.打开或新建一个数据库sqlite3 *db;if (sqlite3_open("./stu.db", &db) != 0){fprintf(stderr, "sqlite3_open:%s", sqlite3_errmsg(db));return -1;}printf("open ok.\n");//2.创建表char *errmsg = NULL;if (sqlite3_exec(db, "create table stu1(id int primary key,name char,score float);",NULL, NULL, &errmsg) != 0){fprintf(stderr, "create table err:%s", errmsg);// return -1;}printf("create table ok.\n");//3.向表中插入数据int id, num;char name[32];float score;char sql[128];printf("please input student number:");scanf("%d", &num);for (int i = 0; i < num; i++){scanf("%d %s %f", &id, name, &score);sprintf(sql, "insert into stu1 values(%d,\"%s\",%f);", id, name, score);if (sqlite3_exec(db, sql, NULL, NULL, &errmsg) != 0){fprintf(stderr, "insert err:%s", errmsg);return -1;}}//4.查询表// if (sqlite3_exec(db, "select id,score from stu1 where id=1;", callback, "hello", &errmsg) != 0)// {// fprintf(stderr, "select err:%s", errmsg);// return -1;// }//只用于查询的函数 sqlite3_get_tablechar **rstp = NULL;int hang, lie;if (sqlite3_get_table(db, "select * from stu1 where id=1;", &rstp, &hang, &lie, &errmsg) != 0){fprintf(stderr, "select err:%s", errmsg);return -1;}int k = 0;for (int i = 0; i < hang + 1; i++){for (int j = 0; j < lie; j++){printf("%s ", rstp[k++]);}putchar(10);}//5.关闭数据库sqlite3_close(db);return 0;
}
callback函数的使用
#include <stdio.h>
//(a+b) * c
//(a-b) * c
//(a*b) * c
//(a/b) * c
//a+b a-b a*b a/bint add(int a,int b)
{return a+b;
}
int sub(int a,int b)
{return a-b;
}
int mul(int a,int b)
{return a*b;
}
int chu(int a,int b)
{return a/b;
}
//int (* fun_p)(int,int);
//fun_p=add; fun_p = mul;
int fun(int c,int (*fun_p)(int ,int ), int a,int b)
{return fun_p(a,b)*c;
}
//想通过一个函数更改一个变量的值:
//两种- 参数(传变量地址),返回值
#if 0
int func(void)
{return 1000;
}
void func1(int *sp)
{*sp=1000;
}
#endifint *func(void)
{static int a=1000;return &a;
}
void func1(int **sp)
{static int a=1000;*sp=&a;
}int main(int argc, const char *argv[])
{printf("%d\n",fun(10,add,2,3));printf("%d\n",fun(10,sub,2,3));printf("%d\n",fun(10,mul,2,3));printf("%d\n",fun(10,chu,2,3));// int a;//1000;
// a=fun();//fun1(&a);int *p;int **q;return 0;
}