查看文件大小du -sh filename
查看磁盘相关信息df
vim 中 dG命令删除当前行到最后
md5sum 文件名:查看文件的md5码,可以唯一标识一个文件
vim中 :开始行,结束行d 删除中间的行
网络通信才是更广泛的通信方式。
两台主机通过ip地址连接。
网络通信其实就是两台主机之间的进程通信。是通过端口号找到进程。
由一个交换机与连接在该设备上的若干个主机就构成了一个局域网。
通信双方必须遵守的规则:协议
网络协议分为两种模型:
为什么要把协议分层?
分层之后,每一层只负责完成自己的任务,下层协议向上层协议提供了基础服务,上层协议使用下层协议, 不关心底层的实现细节,只需要完成各自的职能。
协议除了分层,还分为公有协议和私有协议
tcp/ip是一类协议,是协议族
https:http协议+ssl(安全套接字协议)
传输层两个重要协议:
每一台设备都由唯一的一个物理地址。
端口号的范围:[0 - 2的16次方)
ipv4占4个字节,ipv6占16个字节、物理地址占6个字节,端口号占2个字节
端口号:
以太网是现有局域网最常使用的通信协议标准。
ARP地址转换协议:把ip地址转换成物理地址(mac)
arp缓存表:记录主机的ip和主机物理地址的对应关系
生存时间:TTL(time to live)指的是跳数
六度拓朴理论
路由器:连接两个或多个网络的硬件设备,可以解析,各种不同类型的网络传来的数据包的目的地址,在根据特定的路由算法把各数据包按照最佳路线传送到指定位置。工作在网络层。
路由表:记录两台主机之间最优的路径(类似于高德地图)
交换机:是一种基于mac地址识别,完成以太网数据帧转发的网络设备。工作在网络接口层。
面向连接:正在建立连接、还是正在连接、或者正在关闭连接的状态。
tcp为保证数据安全可靠的传输:序列号、确认重传(重传的次数一般为10次)、滑动窗口(规定通信双方传输数据的大小)(发送方与接收方的窗口大小不一定是一样大)。
ack(Ack)确认号;ACK:确认标志位,表示确认数据信息的,tcp协议规定,三次握手建立完成之后,必须带ACK = 1标志位。
SYN标志位:当SYN=1,表示请求建立连接
FIN标志位:当FIN=1, 表示请求断开连接
序列号其实是一个随机值:
ISN = M+F (序列号生成算法)
wireshark显示相对序列号的设置方式:编辑->首选项->protocols->tcp->勾选relative sequence numbers。
确认号ack是x+1的原因:
三次握手的流程:
tcp协议第三次握手数据包的序列号和第一次发送数据的数据包的序列号是相同的。
两次握手能不能建立连接?
tcp三次握手改成二次握手行不行?
三次握手时报文丢失:
Linux系统下2MSL是60秒
tcp四次挥手:
只有断开方才有TIME-WAIT状态
为什么主动断开方要有TIME-WAIT状态(2MSL)?
四次挥手改成三次挥手行不行?
可以:
1.前提是网络状态非常好.
2.主机B发送完第二次挥手之后,没有数据发送给主机A,那么第二次挥手可以和第三次挥手合并。B->A:FIN=1,ACK=1,seq=v, ack=u+1;
不可以:第三次挥手数据丢失,主机B断开连接,主机A不知道B已断开连接,会等待数据,造成资源浪费。
MTU 是网络传输最大报文包 = 数据的长度 + 协议的头长 1500个字节
MSS 是网络传输数据最大值(不包含协议的长度)
1500 - 20(ip头长) - 20(tcp头长) = 1460字节
udp协议的特点:
可以在应用层对upd进行安全性的封装,使其传输数据安全可靠。已经有开源代码实现该功能,分别是:RUDP、RTP、UDP。
协议的选择:
C/S模式是传统的应用模式:含有客户端和服务端
B/S模式是浏览器充当客户端。
什么是套接口(socket):又称为套接字,是操作系统内核中的一个数据结构, 它是网络中的节点进行相互通信的门户 ,是网络进程的ID。是一个文件描述符。
套接口的组成:ip+端口号
NAT技术(网络地址转换技术):内部私有的网络地址转换成合法的网络ip地址。可以有效的解决公网ip分配不足的问题。
一个完整的套接字:协议、本机IP、本机的端口、对端ip、对端的端口。
tcp像管道、udp像消息队列
小端模式:内存的低地址存储数据的低字节,高地址存数据的高字节。
大端模式:内存低地址存储数据的高字节,高地址存放数据低字节。
IBM公司最初存储数据采用的是大端模式。因特尔、AMD采用小端模式。
主机字节序:电脑采用什么方式存储数据的。
网络字节序:二进制值,大端模式。
在编程中,需要把主机字节序转换成网络字节序。
网络端口的主机字节序和网络字节序的相互转换
uint16_t htons(uint16_t hostshort);
uint16_t ntohs(uint16_t netshort);
IPV4中ip地址主机字节序和网络字节序的相互转换
//主机字节序转为网络字节序
in_addr_t inet_addr(const char *cp);
//成功返回,转换的结果
//参数是ip地址
int inet_aton(const char *cp, struct in_addr *inp);
//参数1:需要转换的ip地址
//参数:保存转换后的网络ip地址。
//网络字节序转换为主机字节序
char *inet_ntoa(struct in_addr in)
//结构体原型:
struct in_addr{
in_addr_t s_addr; //in_addr_t其实就是unsigned int
}
兼容IPV4和IPV6的IP地址转换函数
int inet_pton(int family, const char *src, void *dst);
const char *inet_ntop(int family, const void *src, char *dst, socklen_t len);
//参数1:地址类型,AF_INET表示是ipv4协议,AF_INET6,表示ipv6协议
//参数2:需要转换的网络字节序,是in_addr类型
//参数3:保存转换后的结果
//参数4:保存结果的大小
名字地址转换(域名和IP地址的相互转换)
//函数原型:
struct hostent* gethostbyname(const char* hostname);
struct hostent* gethostbyaddr(const char* addr, size_t len, int family);
//结构体原型:
struct hostent {
char *h_name; /*正式主机名*/
char **h_aliases; /*主机别名*/
int h_addrtype; /*主机 IP 地址类型IPv4为AF_INET,ipv6为AF_INET6*/
int h_length; /*主机 IP 地址字节长度,对于 IPv4 是 4 字节,即 32 位*/
char **h_addr_list; /*主机的 IP 地址列表*/
}
//使用举例
#include
int main(int argc, char **argv)
{
struct hostent* pHost = gethostbyname(argv[1]);
if(NULL == pHost){
return -1;
}
printf("h_name = %s\n", pHost->h_name);
for(int i = 0; pHost->h_aliases[i] != NULL; ++i){
printf("alias = %s\n", pHost->h_aliases[i]);
}
printf("type = %d\n", pHost->h_addrtype);
printf("len = %d\n", pHost->h_length);
char buf[64] = {0};
for(int i = 0; pHost->h_addr_list[i] != NULL; ++i){
memset(buf, 0, sizeof(buf));
inet_ntop(pHost->h_addrtype, pHost->h_addr_list[i], buf, sizeof(buf));
printf("buf = %s\n", buf);
}
return 0;
}
1.服务端流程
int socket(int domain, int type, int protocol);
使用该函数生成一个套接口、用于与客户端建立连接时使用的。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
绑定本机的ip和端口号,绑定到sfd(socket函数返回值) 。
成功返回0,失败返回-1
参数1:socket函数的返回值
参数2:保存ip和端口的结构体
参数3:结构体的大小
//结构体原型:
struct sockaddr {
unsigned short sa_family; /*地址族*/
char sa_data[14]; /*14 字节的协议地址,包含该 socket 的 IP 地址和端口号。*/
};
struct sockaddr_in {
short int sin_family; /*地址族*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP 地址*/
unsigned char sin_zero[8]; /*填充 0 以保持与 struct sockaddr 同样大小*/
};
int listen(int sockfd, int backlog);监听在sfd上的客户端的连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);接收对端的连接
太浪漫了:accept()函数准备好了,系统调用 accept()会有点古怪的地方的!你可以想象发生这样的事情:有人从很远的地方通过一个你在侦听(listen())的端口连接(connect())到你的机器。它的连接将加入到等待接受(accept())的队列中。你调用accept()告诉它你有空闲的连接,它将返回一个新的套接字文件描述符!这样你就有两个套接字了,原来的一个还在侦听你的那个端口,新的在准备发送(send())和接收(recv())数据。这就是这个过程!
ssize_t recv(int sockfd, void *buf, size_t len, int flags);从对端接收数据,该函数是一个阻塞性函数;
ssize_t send(int sockfd, const void *buf, size_t len, int flags); 向对端发送数据
int close(int fd); 关闭文件描述符
2.客户端的流程
socket
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 连接服务器
send
recv
close
3.服务器客户端交流(应用select)
select的底层实现:位图,只能保存0和1。FD_ZERO初始化时,位图所有的位为0。
FD_SET:把需要监听的描述符加入到集合当中,位图所对应的位置为1。
如果select监控到描述符就绪,他就不去修改该描述符在位图所对应值。如果该文件描述符没有就绪,select会将该文件描述符所对应位图的值置为0。
tcp_chat聊天结束后(客户端ctrl+c),服务端疯狂打印(newFd就绪)
原因:客户端主动断开,处于close_wait状态,所以内核检测到与newFd连接的对端断开了,内核会使用newFd告诉服务端,对方已经断开,你也可以断开了。
查看网络相关状态命令: netstat -nat
客户端开的端口又叫临时端口,这个端口范围大于10000, 小于65535.
编程中goto尽量少使用
tcp建立三次握手的时候,内核做了哪些事情。
客户端发送第一次握手,请求建立连接,服务端接收第一次握手,并且回复第二次握手,回复完的同时,把未建立完的tcp连接加入到半连接队列里面(其实是将两次数据包加入);当客户端回复第三次握手的时候,服务端从半连接队列里面取出第一个连接与第三次握手合并成一个完整的tcp连接,并且加入到全连接队列里面。
listen的第二个参数就是指定全连接队列(半连接)的大小。
全连接队列(半连接)大小是由listen的第二个参数与 /proc/sys/net/core/somaxconn这个配置共同决定(取二者最小值)。
查看backlog的最大值 cat /proc/sys/net/ipv4/tcp_max_syn_backlog
设置套接口选项
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
//成功返回0,失败返回-1
//参数1:sfd
//参数2:设置层次
//参数3:设置的选项
//参数4:设置的选项是否生效,大于0生效
//参数5:对参数取sizeof
//参数2选择SOL_SOCKET,对应参数3选择SO_REUSEADDR表示允许重用本地地址和端口
SOL_SOCKET层次的选项:
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
获取缓冲区大小,获取的结果是缓冲区大小的二倍。
//应用
//server.c
#include
int main(int argc, char **argv)
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sfd, -1, "socket");
//保存本机的ip和端口
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
serAddr.sin_port = htons(atoi(argv[2]));
//设置地址可重用
int ret = 0;
int reuse = 1; //设置值大于0,表示生效
ret = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(int));
//绑定本机的IP和端口,绑定到sfd上
int ret = 0;
ret = bind(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
//监听的最大连接数是10
ret = listen(sfd, 10);
ERROR_CHECK(ret, -1, "listen");
//接收连接,返回新的文件描述符,新的描述符作用,与对端进行数据交互使用的
int newFd = 0;
//创建监听集合
fd_set rdset;
fd_set needMonitorSet;
FD_ZERO(&needMonitorSet);
FD_ZERO(&rdset);
FD_SET(STDIN_FILENO, &needMonitorSet);
FD_SET(sfd, &needMonitorSet);
char buf[64] = {0};
int readyNum = 0;
while(1){
//把需要监听的描述符加入到集合当中
memcpy(&rdset, &needMonitorSet, sizeof(fd_set));
readyNum = select(10, &rdset, NULL, NULL, NULL);
ERROR_CHECK(readyNum, -1, "select");
for(int i = 0; i < readyNum; ++i){
//如果是标准输入就绪,就代表需要发送数据给对端
if(FD_ISSET(STDIN_FILENO, &rdset)){
memset(buf, 0, sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));
send(newFd, buf, strlen(buf)-1, 0);
}
//如果是newFd就绪,就说明对端有数据发送给我们
if(FD_ISSET(newFd, &rdset)){
memset(buf, 0, sizeof(buf));
ret = recv(newFd, buf, sizeof(buf)-1, 0);
if(0 == ret){
close(newFd);
FD_CLR(newFd, &needMonitorSet);
printf("bey bey\n");
continue;
}
printf("buf = %s\n", buf);
}
//说明有连接到来
if(FD_ISSET(sfd, &rdset)){
newFd = accept(sfd, NULL, NULL);
ERROR_CHECK(newFd, -1, "accept");
printf("client connect\n");
FD_SET(newFd, &needMonitorSet);
}
}
}
close(sfd);
close(newFd);
return 0;
}
//client.c
#include
int main(int argc, char **argv)
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sfd, -1, "socket");
//保存本机的ip和端口
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
serAddr.sin_port = htons(atoi(argv[2]));
int ret = 0;
ret = connect(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "connect");
//创建监听集合
fd_set rdset;
FD_ZERO(&rdset);
char buf[64] = {0};
int readyNum = 0;
while(1){
//把需要监听的描述符加入到集合当中
FD_SET(STDIN_FILENO, &rdset);
FD_SET(sfd, &rdset);
readyNum = select(sfd + 1, &rdset, NULL, NULL, NULL);
for(int i = 0; i < readyNum; ++i){
//如果是标准输入就绪,就代表需要发送数据给对端
if(FD_ISSET(STDIN_FILENO, &rdset)){
memset(buf, 0, sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));
send(sfd, buf, strlen(buf)-1, 0);
}
//如果是sfd就绪,就说明对端有数据发送给我们
if(FD_ISSET(sfd, &rdset)){
memset(buf, 0, sizeof(buf));
recv(sfd, buf, sizeof(buf)-1, 0);
printf("buf = %s\n", buf);
}
}
}
close(sfd);
return 0;
}
1.服务端
socket
bind
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
//从对端接收数据,并且保存对端的ip和端口
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)
//向指定的ip和端口的对端发送数据
close
2.客户端
socket
sendto
recvfrom
close
对于upd通信来讲,服务端必须要先recvfrom,然后sendto
因为,udp时非面向连接的,所以最开始不知道对端的ip和端口,所以先接收对方发来的而数据,并且记录对端的ip和端口。
//server.c
#include
int main(int argc, char **argv)
{
//udp的通信
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(sfd, -1, "socket");
//保存本机的ip和端口
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
serAddr.sin_port = htons(atoi(argv[2]));
//绑定本机的IP和端口,绑定到sfd上
int ret = 0;
ret = bind(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "bind");
struct sockaddr_in cliAddr;
socklen_t len = sizeof(cliAddr);
memset(&cliAddr, 0, len);
fd_set rdset;
FD_ZERO(&rdset);
int readyNum = 0;
char buf[64] = {0};
while(1){
FD_SET(STDIN_FILENO, &rdset);
FD_SET(sfd, &rdset);
readyNum = select(sfd + 1, &rdset, NULL, NULL, NULL);
ERROR_CHECK(readyNum, -1, "select");
for(int i = 0; i < readyNum; ++i){
if(FD_ISSET(STDIN_FILENO, &rdset)){
memset(buf, 0, sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));
ret = sendto(sfd, buf, strlen(buf)-1, 0, (struct sockaddr*)&cliAddr, len);
ERROR_CHECK(ret, -1, "send");
}
if(FD_ISSET(sfd, &rdset)){
memset(buf, 0, sizeof(buf));
ret = recvfrom(sfd, buf, sizeof(buf)-1, 0, (struct sockaddr*)&cliAddr, &len);
ERROR_CHECK(ret, -1, "recv");
printf("buf = %s\n", buf);
}
}
}
close(sfd);
return 0;
}
//client.c
#include
int main(int argc, char **argv)
{
//udp的通信
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(sfd, -1, "socket");
//保存本机的ip和端口
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
serAddr.sin_port = htons(atoi(argv[2]));
fd_set rdset;
FD_ZERO(&rdset);
socklen_t len = sizeof(serAddr);
int ret = 0;
int readyNum = 0;
char buf[64] = {0};
while(1){
FD_SET(STDIN_FILENO, &rdset);
FD_SET(sfd, &rdset);
readyNum = select(sfd + 1, &rdset, NULL, NULL, NULL);
ERROR_CHECK(readyNum, -1, "select");
for(int i = 0; i < readyNum; ++i){
if(FD_ISSET(STDIN_FILENO, &rdset)){
memset(buf, 0, sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));
ret = sendto(sfd, buf, strlen(buf)-1, 0, (struct sockaddr*)&serAddr, len);
ERROR_CHECK(ret, -1, "send");
}
if(FD_ISSET(sfd, &rdset)){
memset(buf, 0, sizeof(buf));
ret = recvfrom(sfd, buf, sizeof(buf)-1, 0, (struct sockaddr*)&serAddr, &len);
ERROR_CHECK(ret, -1, "recv");
printf("buf = %s\n", buf);
}
}
}
close(sfd);
return 0;
}
什么是epoll?为什么要引入epoll?
select在使用的过程的缺点:
epoll是如何改进的?
把需要监听的文件描述符加入红黑树时,内核会给每个文件描述符注册回调函数,当该描述符就绪时,内核会调用该回调函数,将就绪的文件描述符加入到双向链表中。
epoll的使用
int epoll_create(int size);
//成功返回一个文件描述符(管理后续需要监听的文件描述符),失败返回-1,
//参数:必须填大于0的值
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//成功返回0,失败返回-1
//参数1:epoll_create()的返回值epfd
//参数2:操作epfd,EPOLL_CTL_ADD增加、EPOLL_CTL_MOD修改、EPOLL_CTL_DEL删除
//参数3:需要操作的文件描述符
//参数4:告诉内核操作文件的事件
//结构体原型:
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
events 可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
//成功返回就绪文件描述符的数量,失败返回-1
//参数1:epfd
//参数2:结构体数组,保存就绪文件描述符
//参数3:结构体数组的大小
//参数4:epoll等待时间,如果填-1,表示epol无限等待,直到有就绪的文件描述符。
查看Ubuntu下,epoll能监控的文件描述符的数量命令: cat /proc/sys/fs/file-max
epoll的两种触发模式
LT 模式:Level_trigger(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写;
ET 模式:Edge_trigger(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你,ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比 LT模式高。
epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读阻塞写操作把处理多个文件描述符的任务饿死。
修改某个文件描述符为边缘触发模式,将该描述符加入epoll之前的events事件的events属性|EPOLLET就可以。
在ET模式下,当接收缓冲区比较小,需要循环接收数据,且把recv设置为非阻塞的,设置为非阻塞的后当接收完数据后,recv会返回-1,此时需判断退出循环。
ET模式一般要配合非阻塞接口去使用 。设置非阻塞的方式如下
fcntl函数
int fcntl(int fd, int cmd, ... /* arg */ );
//修改(获取)文件描述符属性
//参数1:需要修改的文件描述符,
//参数2:修改(获取)文件描述符的操作
//可变参数:设置的属性
fcntl 函数有 5 种功能:
1.复制一个现有的描述符(cmd=F_DUPFD)
2.获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD)
3.获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL)
4.获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN)
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK 或 F_SETLKW)
F_SETFL 设置文件描述符状态旗标,参数 arg 为新旗标,但只允许 O_APPEND、O_NONBLOCK 和 O_ASYNC 位的改变,其他位的改变将不受影响。
函数的阻塞性和文件描述符属性有关。
epoll原理刨析
添加、修改、删除节点到红黑树,需要不要查找?
需要查找,logn
添加描述符到红黑树上时,有一次内存拷贝,从用户空间拷贝到内核空间,一次注册文件描述符,永久生效
当描述符添加到红黑树上时,内核会给每一个节点注册一个回调函数,当该描述符就绪的时候,会主动调用自己的回调函数,将其加入到双向链表当中。
//epoll应用于服务器
#include
void setNoBlock(int fd)
{
int status = 0;
status = fcntl(fd, F_GETFL);
status |= O_NONBLOCK;
fcntl(fd, F_SETFL, status);
}
int main(int argc, char **argv)
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sfd, -1, "socket");
//保存本机的ip和端口
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(argv[1]);
serAddr.sin_port = htons(atoi(argv[2]));
//绑定本机的IP和端口,绑定到sfd上
int ret = 0;
ret = bind(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
//监听的最大连接数是10
ret = listen(sfd, 10);
ERROR_CHECK(ret, -1, "listen");
//接收连接,返回新的文件描述符,新的描述符作用,与对端进行数据交互使用的
int newFd = accept(sfd, NULL, NULL);
ERROR_CHECK(newFd, -1, "accept");
setNoBlock(newFd);
//创建epoll,参数必须大于0
int epfd = epoll_create(1);
ERROR_CHECK(epfd, -1, "epoll_create");
struct epoll_event events, evs[2];
memset(&events, 0, sizeof(events));
//告诉内核监听读事件
events.events = EPOLLIN;
events.data.fd = STDIN_FILENO;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &events);
ERROR_CHECK(ret, -1, "epoll_ctl");
events.events = EPOLLIN|EPOLLET;
events.data.fd = newFd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, newFd, &events);
ERROR_CHECK(ret, -1, "epoll_ctl");
char buf[4] = {0};
int readyNum = 0;
while(1){
readyNum = epoll_wait(epfd, evs, 2, -1);
printf("after wait\n");
for(int i = 0; i < readyNum; ++i){
//如果是标准输入就绪,就代表需要发送数据给对端
if(evs[i].data.fd == STDIN_FILENO){
memset(buf, 0, sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));
send(newFd, buf, strlen(buf)-1, 0);
}
//如果是newFd就绪,就说明对端有数据发送给我们
if(evs[i].data.fd == newFd){
memset(buf, 0, sizeof(buf));
while(1){
//第一种方式设置recv为非阻塞
/* ret = recv(newFd, buf, sizeof(buf)-1, MSG_DONTWAIT); */
//使用fcntl函数将newFd设置为非阻塞性描述符,可以让recv函数
//实现非阻塞的
ret = recv(newFd, buf, sizeof(buf)-1, 0);
if(0 == ret){
close(newFd);
printf("bey bey\n");
continue;
}
if(-1 == ret){
break;
}
printf("buf = %s\n", buf);
}
}
}
}
close(sfd);
close(newFd);
return 0;
}
select总结
epoll的总结
epoll的效率一定比select高?
epoll的超时时间:在第四个参数填,单位是毫秒,1秒=1000毫秒。
epoll设置超时时间的作用:为了断开已经没有和服务端有数据交互的客户端,节约系统资源。
SYN Flood攻击:攻击者利用了tcp协议的漏洞,只发送第一次握手,服务端回复第二次握手,并且把这个半连接状态加入到半连接队列里。攻击者不回复第三次握手,造成服务端半连接队列满,无法接收正常客户端的连接请求。
如何防护SYN Flood攻击
UDP Flood攻击,发UDP小包
ICMP Flood攻击
缓冲区的大小默认64k,所以一个tcp连接系统至少要分配128k的空间。如过业务逻辑每次的数据量比较少,那么可以减小缓冲区的大小,来节约服务器成本,增加连接数。 使用函数:
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
//成功返回0,失败返回-1
//参数1:sfd
//参数2:设置层次
//参数3:设置的选项
//参数4:设置的选项是否生效,大于0生效
//参数5:对参数取sizeof
SOL_SOCKET层次的选项:
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDLOWAT 发送缓冲区下限 int
获取缓冲区大小,获取的结果是缓冲区大小的二倍。
五种io模型
阻塞IO(blocking I/O)
A拿着一支鱼竿在河边钓鱼,并且一直在鱼竿前等,在等的时候不做其他的事情,十分专心。只有鱼上钩的时,才结束掉等的动作,把鱼钓上来。
非阻塞IO(noblocking I/O)
B也在河边钓鱼,但是B不想将自己的所有时间都花费在钓鱼上,在等鱼上钩这个时间段中,B也在做其他的事情(一会看看书,一会读读报纸,一会又去看其他人的钓鱼等),但B在做这些事情的时候,每隔一个固定的时间检查鱼是否上钩。一旦检查到有鱼上钩,就停下手中的事情,把鱼钓上来。
信号驱动IO(signal blocking I/O)
C也在河边钓鱼,但与A、B不同的是,C比较聪明,他给鱼竿上挂一个铃铛,当有鱼上钩的时候,这个铃铛就会被碰响,C就会将鱼钓上来。
IO多路复用(I/O multiplexing)
D同样也在河边钓鱼,但是D生活水平比较好,D拿了很多的鱼竿,一次性有很多鱼竿在等,(对于select)而且每个鱼杆上都挂了铃铛,当铃铛响了的时候,D轮询的查找是哪个鱼杆上的铃铛响的。(对于epoll)给每个鱼杆都设置了不同的响铃方式,这样不用轮询,直接到相应鱼杆把鱼拉上来即可
异步IO(asynchronous I/O)
E也想钓鱼,但E有事情,于是他雇来了F,让F帮他钓鱼,一旦有鱼上钩,F就钓鱼上来,然后打电话通知E钓鱼完成。
目的:实现多个客户端同时下载文件
好处:
管道不可以传递文件描述符,只能传递描述符所对应的数值。
进程间传递文件描述符
int socketpair(int domain, int type, int protocol, int sv[2])
//成功返回0,失败返回-1
//参数1:网络协议,AF_LOCAL
//参数2:通信类型,SOCK_STREAM/SOCK_DGRAM
//参数3:协议编号
//参数4:存储创建的套接口
该函数作用,创建一对套接口,这一对套接口创建的时候就是相连,类似于全双工的管道,只能在本机使用。
sendmsg 发送描述符
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
//成功返回发送字节数,失败返回-1
recvmsg接收文件描述符
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
//成功返回接收到的字节数,失败返回-1
结构体原型:
struct msghdr {
void *msg_name; /* optional address套接口地址,应用于 udp */
socklen_t msg_namelen; /* size of address 套接口地址长度 */
struct iovec *msg_iov; /* scatter/gather array 指向 iovec 结构体数组 */
size_t msg_iovlen; /* # elements in msg_iov 结构体数组大小 */
void *msg_control; /* ancillary data, see below 附属数据,指向 cmsghdr 结构体 */
size_t msg_controllen; /* ancillary data buffer len cmsghdr 结构体的长度 */
int msg_flags; /* flags (unused) 标志位,没有使用(用于接收特定的标志位) */
};
//iovec 必须赋值
struct iovec {
void *iov_base;
/* Starting address 指向缓冲区的起始位置,缓冲区用于存储 writev 所接收/要发送的数据 */
size_t iov_len;
/* Number of bytes to transfer 要传输的字节数 */
};
//附属数据结构体 cmsghdr
struct cmsghdr {
socklen_t cmsg_len;
/* data byte count, including header 结构体大小、这个值可由 CMSG_LEN()宏计算 */
int cmsg_level; /* originating protocol 原始协议,填 SOL_SOCKET*/
int cmsg_type; /* protocol-specific type 特定协议类型,填 SCM_RIGHTS */
unsigned char cmsg_data[]; /*可变长数组,存放附属数据,使用 CMSG_DATA()接收 */
}
使用举例:
#include
int sendFd(int pipeFd, int fd)
{
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
struct iovec iov;
memset(&iov, 0, sizeof(iov));
char buf[8] = {0};
strcpy(buf, "a");
iov.iov_base = buf;
iov.iov_len = strlen(buf);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
size_t len = CMSG_LEN(sizeof(int));
struct cmsghdr *cmsg = (struct cmsghdr *)calloc(1, len);
cmsg->cmsg_len = len;
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
*(int*)CMSG_DATA(cmsg) = fd;
msg.msg_control = cmsg;
msg.msg_controllen = len;
sendmsg(pipeFd, &msg, 0);
return 0;
}
int recvFd(int pipeFd, int *fd)
{
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
struct iovec iov;
memset(&iov, 0, sizeof(iov));
char buf[8] = {0};
strcpy(buf, "a");
iov.iov_base = buf;
iov.iov_len = strlen(buf);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
size_t len = CMSG_LEN(sizeof(int));
struct cmsghdr *cmsg = (struct cmsghdr *)calloc(1, len);
cmsg->cmsg_len = len;
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
msg.msg_control = cmsg;
msg.msg_controllen = len;
recvmsg(pipeFd, &msg, 0);
*fd = *(int*)CMSG_DATA(cmsg);
return 0;
}
int main(int argc, char **argv)
{
int fds[2];
int ret = socketpair(AF_LOCAL, SOCK_STREAM, 0, fds);
ERROR_CHECK(ret, -1, "socketpair");
if(fork()){
close(fds[0]);
int fd = open("file", O_RDWR);
ERROR_CHECK(fd, -1, "open");
printf("fd = %d\n", fd);
sendFd(fds[1], fd);
//write(fd, "nihao", 5);
wait(NULL);
}
else{
close(fds[1]);
int nfd = 0;
recvFd(fds[0], &nfd);
printf("nfd = %d\n", nfd);
char buf[64] = {0};
read(nfd, buf, sizeof(buf));
printf("buf = %s\n", buf);
exit(0);
}
return 0;
}
对于一个没有长度的数组叫可变长数组又叫柔性数组;、
写iovec结构体数据使用的函数是:
ssize_t writev(int fd, const struct iovec *iov, int iovcnt)
参数1:往哪写数据
参数2:结构体
参数3:结构体的个数
#include
int main(int argc, char **argv)
{
struct iovec iov[2];
memset(iov, 0, sizeof(iov));
char buf1[64] = {0};
strcpy(buf1, "hello");
iov[0].iov_base = buf1;
iov[0].iov_len = strlen(buf1);
char buf2[64] = {0};
strcpy(buf2, "world");
iov[1].iov_base = buf2;
iov[1].iov_len = strlen(buf2);
writev(STDOUT_FILENO, iov, 2);
return 0;
}
进程池流程
父进程的流程:
循环创建子进程,记录子进程相关信息(子进程id,子进程状态,子进程通信使用的文件描述符)
创建tcp监听套接字,等待客户端的连接
创建epoll,把需要监听的描述符添加到epoll当中
如果客户端连接服务器,使用accept函数接收这次连接请求,返回newFd, 交给空闲子进程
如果epoll监听的管道可读,表示子进程工作完成,把子进程的工作状态置为空闲。
子进程的流程:
客户端想要从服务器上下载文件,该文件该如何传输?
发送方:
接收方:
什么是tcp粘包问题:多次发送发送的数据,可以被一次接收,数据之间没有分界线,所有数据全粘在一起。
如何去解决粘包问题?人为去规定数据的分界线。私有协议。
解决收发数据时发送方和接收方速度不匹配的方法
修改recv函数的第四个参数属性为MSG_WAITALL;
ret = recv(sfd, buf, dataLen, MSG_WAITALL);
封装recv函数
int recvCycle(int sockFd, void *buf, int totalSize)
{
int recvSize = 0;
int ret = 0;
while(recvSize < totalSize){
ret = recv(sockFd, (char *)buf + recvSize, totalSize - recvSize, 0);
recvSize += ret;
}
return recvSize;
}
如果要发送的文件描述符关闭,则send第一次返回-1,之后触发SIGPIPE信号。
打印传输文件进度条,需要知道文件的的长度,所以要获取文件状态
int fstat(int fd,struct stat *statbuf);
//成功返回0,失败返回-1
在打印进度条的方式:
printf("rate = %5.2f%%\r", rate); // “\r”每次输出时从行首输出
fflush(stdout); //防止光标跳动太快
零拷贝
发送时将文件使用mmap映射到内存空间,然后send到发送缓冲区;
接收时将文件创建好后,ftruncate()后,使用mmap映射到内存,然后用recv接收数据时直接接收到映射的地址上。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
//成功返回指向映射空间的指针,失败-1
//参数1:填NULL,让系统指定分配位置
//参数2:映射空间大小
//参数3:期望的内存保护标志 PROT_READ|PROT_WRITE
//参数4:指定映射对象的类型,是否可以被进程共享 MAP_SHARED
//参数5:文件描述符
//参数6:被映射的内容起始位置
使用sendfile来发送数据,不需要把文件读进内存,填文件描述符直接发送到发送缓冲区,把内核空间的数据直接拷贝到发送缓冲区,不经过用户空间;
接收方可使用1.中的接收方法来接。
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
//参数1:往哪个文件描述符写数据
//参数2:从哪个文件描述符取数据
//参数3:文件偏移量
//参数4:传输数据长度
//sendfile只能用于发送方发送数据,不能用于接收数据
创建管道int fds[2]; pipe(fds); 然后循环:使用splice()函数将源文件写到管道,再从管道读出到发送缓冲区。参数len的选择影响传输效率。
ssize_t splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);
//成功返回接收到的字节数,失败-1,
//参数1:从哪取数据
//参数2:偏移量
//参数3:数据写到哪
//参数4:偏移量
//参数5:单次写入长度,最多65536
//参数6:0
使用举例:
int fds[2];
pipe(fds);
int recvLen = 0;
while(recvLen < fileInfo.st_size){
ret = splice(fd, 0, fds[1], 0, 65536, 0);
ret = splice(fds[0], 0, clientFd, 0, ret, 0);
recvLen += ret;
}
实际工作中零拷贝技术用的并不多
获取时间当前时间
int gettimeofday(struct timeval *tv, struct timezone *tz);
//结构体原型
struct timval{
time_t tv_sec;
suseconds_t tv_usec;
}
进程池的退出
两种退出方式:
线程池中线程的数量不是越多越好:以下方案效率高
防止头文件重复包含
#ifndef ...
#define ...
//"code"
#endif
#pragma once
创建结构体时,小类型优先放到前面