目录
万恶的结构体sockaddr、sockaddr_in、in_addr、hostent
Writen、Readn函数:freecplus封装的报文格式
TcpWrite、TcpRead:freecplus封装的报文格式
IP端格式
C/S和B/S模式
网络字节序转换函数
在通信过程中,套接字是成对出现的
inet_pton函数
IP地址开始是点分十进制形式的,是一个字符串,如192.168.1.100,所以要将它转换为网络字节序地址需要这个inet_pton函数:本地字节序-->网络字节序
时间轮
回到时间轮定时器,它主要解决的是升序链表插入效率比较低的问题,根据相关链表算法的理论,因为在有序链表插入节点的时间复杂度为O(n),而且是单链表,意味着链表越长,插一个节点所要找到合适位置的时间开销就会越大,这样下来,时间效率是比较低的。
时间轮定时器算法有点hash的思想,插入节点是采用【取模+头插法】的方式,将插入的平均时间复杂度控制器到了O(1),极大节省了时间开销。
不过呢,升序链表定时器虽然在插入时间复杂度上为O(n),但是在处理超时定时器时遍历链表的效率还是要比时间轮定时器好的,时间轮定时器需要将对应槽上的链表从头到尾全部判断一次,而链表则是从头开始处理,一旦遇到未超时的,则直接结束遍历就好。但综合效率来说,时间轮还是比升序链表好很多的。
Socket:C/S模式,client server
socket函数
int socket(int domain, int type, int protocol);
domain是协议族,type指的是socket的类型,protocol:指定协议。
第一个参数只能填AF_INET,第二个参数只能填SOCK_STREAM,第三个参数只能填0。
除非系统资料耗尽,socket函数一般不会返回失败。
不要忽略任何一个警告(socklen_t*) &socklen)
perror(const char* str)
先输出str: ,再输出错误信息
INADDR_ANY
说明指定的是任意的ip地址
函数调用流程
服务端函数调用的流程
socket->bind->listen->accept->recv/send->close
客户端函数调用的流程
socket->connect->send/recv->close
其中send/recv可以进行多次交互。
无论是客户端还是服务端都要创建socket
万恶的结构体sockaddr、sockaddr_in、in_addr、hostent
hostent
主机字节序与网络字节序
listen、accept、connect函数之间的关系
listen函数
listen(listenfd, 5);//把socket设置为监听模式,第二个参数比较复杂
如果在listen前面加上sleep,结果发现不能连接上
如果在listen后面加上sleep,可以连接上
原因是:服务端在调用listen之前,客户端不能向服务端发起连接请求。
connect函数
connect(sockfd, (struct sockaddr *)&servaddr,sizeof(servaddr))
向服务端发起连接请求
accept函数
clientfd=accept(listenfd,(struct sockaddr *)&clientaddr,(socklen_t*)&socklen);
三个客户端连接,accept是从连接的一个队列里面取出来,如果队列里面没有,会阻塞。
把这个用死循环,才可以多客户端连接
TCP的三次握手
使用netstat -na | more命令查询端口的连接状态,
在listen后面睡眠1000秒,此时启动服务端,就可以查看到ESTABLISHED状态
send、和recv函数
Socket send函数和recv函数详解 - 世道 - 博客园
int send(Socket s, const char* buf, int len, int flags);
int recv(Socket s, char * buf, int len, int flags);
都需要通过socket套接字来实现数据的接受与发送,而不是通过send、recv函数来实现。
其实这两个函数也有不成功的时候,当缓冲区满了之后,就不能正常的接收了。
在发送端使用正常代码,注释掉接受的代码,只用来发送
客户端注释掉发送的代码,只用来接受,而且在接受后面
usleep(10000);//睡眠微秒,结果在接受了八万多条数据之后,缓冲区满了,send函数在发送端阻塞了,等客户端又取出了一些数据之后才继续接受数据。
TCP报文分包和粘包
粘包:粘包是指TCP协议中,发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包的数据的头紧接着前一包数据的尾。TCP是面向字节流的协议,就是没有界限的一串数据,本没有“包”的概念,“粘包”和“拆包”一说是为了有助于形象地理解这两种现象。为什么 UDP没有粘包由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。
对于粘包和拆包问题,常见的解决方案有四种:
- 1)发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
- 2)发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
- 3)将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
- 4)通过自定义协议进行粘包和拆包的处理。
第三种方法是最常用的方法。
Writen、Readn函数:freecplus封装的报文格式
这两个函数指的是recv函数和send函数都存在读取的数据报文不完整的情况
TcpWrite、TcpRead:freecplus封装的报文格式
报文长度+报文内容,这个就是Tcp粘包的一种最常见的解决方法。
这两个函数的参数的详细解释:
进程间通信
六种方式,前两者不经常使用,共享内存没有加锁的机制,所以经常与信号灯结合一起来使用,在高性能的网络服务端程序中,可以用共享内存作为的数据缓存(cache)。
避免僵尸进程
僵尸进程:进程退出的时候,unix将该进程的所有资源释放,但是仍然为其保留一定的信息,包括进程号(process identify),退出状态,运行时间。这个是一种机制,可以让父进程任何时候想知道子进程的状态信息的时候都可以调出来。这些信息的资源需要直到父进程调用wait/waitpid来取信息的时候才释放,但是如果父进程不去调用wait/waitpid,它的进程号就会一直占用,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。
两种解决方式:
僵尸进程是指系统将进程退出的信息保存下来,此时占用一个进程编号,除非调用wait/waitpid,将其释放,否则如果出现过多的僵尸进程会将进程号使用完,孤儿进程是指该进程没有父进程,从而被init进程管理,可以回收资源。
我后面才知道没有模拟出僵尸进程的原因,需要多个客户端连接,使用多进程或者多线程,多进程需要fork函数才可以实现,
有标志的进程就是僵尸进程。
如果按Ctrl+c终止book250后,父进程退出,僵尸进程随之消失。
有时使用ctrl+c的命令结束主进程是不对的,需要kill+pid。
C/C++服务程序的运行日志
tail -f demo47.log 查看日志文件,获取内容
mirac@ubuntu:~/Desktop/网络编程$ tar zxvf freecplus_20200615.tgz -C ./
先解压文件
linux多进程:fork函数
进程是操作系统进行资源分配和调度的一个基本单位。
fork调用后,子进程与父进程是否共享变量:
fork调用后,子进程与父进程是否共享变量_浅浅的i的博客-CSDN博客
不会真的共享变量,虽然地址是一样的,但是这个地址是给程序员看的虚拟地址,真正通过内核映射到实际的物理地址是不一样的。
fork多个子进程:
fork创建多个子进程_XV_的博客-CSDN博客_fork多个子进程
Linux信号的使用
当服务程序运行在后台的时候,直接使用Ctrl+c让程序直接退出是不好的,因为程序突然退出,还有资源没有释放,这个时候signal函数就起作用了。
使用信号函数:sighandler_t signal(int signum, sighandler_t handler);
第一个参数是指signum是信号的编号,第二个参数是指处理信号的函数(可以在里面编写释放资源的代码)。
不需要清楚返回值。
程序员关心的信号有三个:SIGINT、SIGTERM和SIGKILL。
按Ctrl+c,发出SIGINT信号,信号编号是2。
采用“kill 进程编号”或“killall 程序名”,发出的是SIGTERM信号,编号是15。
采用“kill -9 进程编号”,发出的是SIGKILL信号,编号是9,此信号不能被忽略,也无法捕获,程序将突然死亡。
所以,程序员只要设置SIGINT和SIGTERM两个信号的处理函数就可以了,这两个信号可以使用同一个处理函数,函数的代码是释放资源。
demo:signal.cpp
Linux共享内存的函数
头文件:#include <sys/ipc.h> #include <sys/shm.h>
1、shmget:获取或创建共享内存
2、shmat:把共享内存连接到当前进程的地址空间
3、shmdt:该函数用于将共享内存从当前进程中分离,相当于shmat函数的反操作。
4、shmctl:删除共享内存
要使用ipcs -m查看共享内存的信息
一旦运行了程序,使用 ipcrm -m shmid,来删除共享内存,否则会一直存在
Linux多线程
在Linux下,采用pthread_create函数来创建一个新的线程,函数声明:
函数声明:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
动态库与静态库
在使用的时候,都是尽量的在使用动态库,因为静态库占用空间大,静态库是将所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件,其中一个文件修改,整个项目都要重新编译以及用户也要重新下载(最大的缺点)。
argc与argv
./在终端运行代码时,加入的参数存储在argc、argv中,argc是参的个数,argv是参数的值
Makefile
Makefile, Makefile
GDB的简单使用
首先g++中加入-g参数,然后file+可执行文件的名字,run,b,c,一直使用n,最后使用quit退出。如果需要设置参数使用set args
例子:
GDB的例子,通过测试了两个易错的面试题, int a[5]={0,1,2,3,4},&a+1的地址是a+1的地址,即数组的后面一个位置(想象成二维数组)
Tcp长连接与短连接
长连接:建立了一次连接之后需要长时间的保持住,双方需要互相发送检测的包,
流程:三次握手建立连接--》传输数据--》保持连接(心跳)--》发送数据--》保持连接(心跳)……--》4次挥手断开连接。
短连接:建立一次连接,发送完数据之后就断开连接。
流程:3次握手建立连接--》传输数据--》4次挥手断开连接。
由于在建立连接与断开连接的过程中要消耗资源和时间的,所以在不同的场景需要使用不同的方式。
对于服务器来说,服务器里的这些个长连接其实很有数据库连接池的味道,大家都是为了节省连接重复利用嘛。
(史上最通俗!)http请求怎样实现TCP长连接,以及长轮询和短轮询的区别。
长轮询和短轮询(针对淘宝的库存量的显示的场景)
网络服务性能测试
主要的性能指标:
1、服务端的并发能力
2、服务端的业务处理能力
3、客户端接收响应的时效
4、网络带宽
I/O复用
有三种模式:select、poll、epoll,各有适合的应用场景。
env | grep LANG//查看使用的编码格式
彻底搞懂 Select / Poll / Epoll,就这篇了!
select
socket的集合fd_set用于存放多个socket,使用select来阻塞程序,而不是在分别的recv、read、send、accept阻塞。
在tcpSelect.cpp中while(1)循环里面fd_set使用的是temp_set,因为会改变原来的read_set,只做了一个基本的了解,时间可以的话要在听一遍。
poll
优化了参数的设置,较为简单,但是不能跨平台,效率与select差不多
不需要备份fd_set,原因就在结构体pollfd中,有一个revent(return event)作为备份。
epoll
结构体
ET和LT
测试水平触发:
- 事件没有处理:int xx=0; 作为计数器,指定xx=2的时候continue,然后这个事件就没有处理,看看epoll的处理方式(默认是水平触发,所以会立即报告)。
- 事件只处理了一般:read函数sizeof(buffer),设置成小于它的数字,这个时候缓冲区就会剩余数据没有处理完,epoll也会立即处理完。
一样的测试方法:
需要将accept函数设置为非阻塞的模式setnoblocking
参考
C/C++网络编程,从socket到epoll_哔哩哔哩_bilibili
上面的大部分代码都改自于网络通信基础socket