1、统筹
学习目标:
C/C++、python精通。
就业匹配方向:专精一个领域,延长职业生涯。
(1)适配行业;
(2)量化;
(3)安全;
(4)高性能;
(5)cdn;
(6)游戏服务;
(7)基础架构;
(8)虚拟化;
(9)网路开发;
(10)存储;
(11)自动驾驶;
(12)推荐算法;
(13)流媒体服务;
(14)金融业务;
【1】C/C++学习重点:
(1)数据结构/算法;(hash、rbtree、b-/b+ tree、list)
(2)设计模式;(见设计模式专栏,要求全精)
(3)库;(STL、C++新特性)
(4)Linux工程;(CMake、git、tcp抓包、netstat)
【2】软件设计基础组件:
(1)内存池;
(2)线程池;
(3)数据库连接池;
(4)请求池;
(5)原子操作;
(6)ringbuffer;
(7)无锁队列;
(8)定时器方案;
(9)死锁检测;
(10)内存泄露;
(11)日志;
(12)网络块
(13)共享内存;
(14)probuf;
【3】软件设计中间组件:
(1)mysql;
(2)redis;
(3)nginx;
(4)grpc;
(5)mq;(消息队列)
【4】框架:适配行业
(1)cuda;(适配高性能计算)
(2)spdk;(适配存储)
(3)skynet;(适配游戏行业)
(4)dpdk;(适配网络)
【5】网络方面:
(1)网络编程;(select、poll、epoll;多进程多线程;阻塞非阻塞;同步异步;业务)
(2)网络原理;(eth、ip、udp/tcp、http)
(3)实现一个网络框架;实现一个TCP/IP协议栈;
【6】运维和部署:
(1)docker;
(2)k8s;
【7】性能分析:需要对内核有了解
(1)磁盘;
(2)网络;
(3)内存;
【8】分布式
(1)分布式数据库 TiDB;
(2)分布式文件系统 ecph;
(3)分布式协同 etcd;
注解:
【1】【2】【3】【4】【5】作为技术基础;【6】【7】作为技术实现的产品;【8】作为技术扩展。
硬实力:技术过硬。
软实力:组织能力;协调能力;沟通能力等。
运气。
硬实力 + 软实力 + 运气。
2、数据结构与算法
学习目标:
队列、栈、链表、环形链表、双向链表、二叉树、红黑树、2-3-4、B树、B+树、矩阵等。
2.1、B树
B树、B-树通常指的是同一种数据结构。是一种平衡的多路搜索树。
定义:一棵m阶的B树满足下列条件:
(1)每个结点至多有m棵子树。【阶数 m
决定了每个节点中孩子节点的最大数量和关键字的最大数量】
(2)除根结点外,其它每个分支结点至少有ceil(m/2)棵子树。
(3)根结点至少有两棵子树(除非B树只包含一个结点)。
(4)所有叶子结点在同一层上。B树的叶子结点可以看成一种外部结点,不包含任何信息。
(5)有k个孩子的非叶子结点恰好有k-1个关键字,关键码按递增次序排列。【即一个节点最多有 m
个子节点和 m-1
个关键字。】
#define m 1024
struct BTNode;
typedef struct BTNode *PBTNode;
struct BTNode {
int keyNum; //实际关键字个数,keyNum < m
PBTNode parent; //指向父亲节点
PBTNode *ptr;
keyType *key; //关键字向量
}
typedef struct BTNode *BTree;
typedef BTree *PBTree;
插入操作:图示:以5阶B树为例:根据B树的定义,节点最多有4个关键字,最少有2个关键字:
(1)在空树插入39,此时就有一个值,根结点也是叶子结点;
(2)继续插入22,97和41值,根结点变为4个值,符合要求;
(3)插入51;插入之后发现超过结点最多只有4个值的限制,
所以要以中间值进行分开,分开后当前结点要指向父结点,分裂之后,发现符合要求;
(4)插入13,21,40,同样造成分裂;
(5)紧接着插入30,27,33,36,24,34,35,29
(6)将26再次插入进去,需要再次分裂;
(7)最后插入17,28,31,32;
删除操作:
【1】如果删除的是叶子节点上的数据,删除之后移动元素保证该叶子节点顺序不变。
【1.1】如果此时关键字个数依然满足条件,则删除结束。
【】1.2】如果此时关键字个数小于条件个数,则需要进行移动。
【1.2.1】首先看其相邻兄弟节点是否有富余关键字[>ceil(M / 2)-1],如果有,则将父节点的合适的关键字下移到当前要删除字节点内的最小位置,注意此处放入的时候需要保持顺序,然后将富余节点的最左(最右)关键字上移到父节点中,然后删除该关键字。
【1.2.2】如果其相邻兄弟节点没有富余关键字了,则需要将该节点和相邻节点进行合并(选择左右没关系,但是要保证顺序的一致就行),移动的规则是将父节点的中间元素(父节点必须在两个需要合并的节点之间)下移到改节点中被删除的关键字处,然后记性合并操作,操作之后可能父节点能满足数目条件,如果不满足的话,仍然再次执行1.2.1 --> 1.2.2的步骤,简要的说就是先看相邻兄弟节点是否富余,富余的话借父节点,不富余的话就合并。
【2】如果删除的是非叶子节点上的数据,非叶子节点特殊性在于它存在孩子节点。
【2.1】首先将该关键字的后继节点中的最左边上移到该位置(这个最左边是指中序遍历后继节点中最左边的节点),如果后继节点上移一个关键字之后满足数目条件,则结束。
【2.2】后继节点数目不满足条件数目了,则需要观察能否进行借的操作,如果不能借,则需要合并,和叶子节点操作类似。
(1)原始状态;
(2)在上面的B树中删除21,删除之后结点个数大于等于2,所以删除结束;
(3)删除27;
27处于非叶子结点,用27的后继替换,也即是28替换27,然后在右孩子结点删除28。发现删除后当前叶子结点的记录的个数已经小于2,而兄弟结点中有3个记录,可以从兄弟结点中借取一个key,父结点中的28就下移,兄弟结点中的26就上移,删除结束。
(4)删除32;
删除之后发现,当前结点中有key,而兄弟都有两个key,所以只能让父结点的30下移到和孩子一起合并,成为新的结点,并指向父结点,经拆封发现符合要求
示例:
// btree.h
#pragma once
#include<iostream>
#include <queue>
template<typename T, int M>
struct BTreeNode
{
T data[M]; //数据数组【一个节点的关键字数组】【M是一个节点的最大关键字个数】
int count; //节点个数【一个节点的关键字个数】
BTreeNode<T, M>* childs[M + 1]; //孩子节点【一个节点的字节点数组,M+1是一个节点的最大子节点个数】
BTreeNode<T, M>* parent; //父节点【一个节点的父节点,only one】
BTreeNode()
{
count = 0;
for (int i = 0; i < M + 1; i++)
{
childs[i] = nullptr;
}
for (int i = 0; i < M; i++)
{
data[i] = -1;
}
parent = nullptr;
}
};
template<typename T, int M>
class BTree
{
public:
BTree();
//插入节点
void InsertElem(T e);
//删除节点
void DeleteElem(T e);
//层序遍历
void LevelTraverse();
//查询某一个数据
BTreeNode<T, M>* SearchElem(T e, int& index);
private:
//插入节点
void InsertElem(BTreeNode<T, M>*& Node, T e);
//将数据插入到节点中
void InsertDataToNode(BTreeNode<T, M>*& Node, T e);
//删除对应数据的节点
void DeleteElem(BTreeNode<T, M>*& Node, T e);
//查找对应数据的节点
BTreeNode<T, M>* SearchElem(BTreeNode<T, M>* Node, T e, int& FindIdx);
//在节点中删除该数据
void DeleteDataFromNode(BTreeNode<T, M>*& Node, int Index);
void DeleteChildFromNode(BTreeNode<T, M>*& Node, int Index);
//层序遍历B树
void LevelTraverse(BTreeNode<T, M>* Node);
//获取当前节点的高度
int GetNodeHigh(BTreeNode<T, M>* Node);
//根节点
BTreeNode<T, M>* m_root;
};
// btree.cpp
#include<iostream>
#include<cstdlib>
#include<ctime>
#include "btree.h"
template<typename T, int M>
BTree<T, M>::BTree()
{
m_root = nullptr;
}
template<typename T, int M>
void BTree<T, M>::InsertElem(T e)
{
std::cout << std::endl << "InsertElem:" << e << std::endl;
InsertElem(m_root, e);
}
template<typename T, int M>
void BTree<T, M>::DeleteElem(T e)
{
std::cout << std::endl << "DeleteElem:" << e << std::endl;
DeleteElem(m_root, e);
}
template<typename T, int M>
void BTree<T, M>::LevelTraverse()
{
LevelTraverse(m_root);
std::cout << std::endl;
}
template<typename T, int M>
BTreeNode<T, M>* BTree<T, M>::SearchElem(T e, int& index)
{
return SearchElem(m_root, e, index);
}
template<typename T, int M>
void BTree<T, M>::InsertElem(BTreeNode<T, M>*& Node, T e)
{
//如果是根节点,直接获取值
if (nullptr == Node)
{
Node = new BTreeNode<T, M>;
Node->data[Node->count++] = e;
return;
}
BTreeNode<T, M>* pCurNode = Node;
BTreeNode<T, M>* pFatherNode = nullptr;
//找到待插入节点的位置
while (nullptr != pCurNode)
{
int index = 0;
while (index < pCurNode->count)
{
//插入一样的数据,直接返回
if (pCurNode->data[index] == e)
{
return;
}
else if (pCurNode->data[index] < e)
{
index++;
}
else
{
break;
}
}
pFatherNode = pCurNode;
pCurNode = pCurNode->childs[index];
}
pCurNode = pFatherNode;
//插入数据
InsertDataToNode(pCurNode, e);
//判断数据是否超过最大范围
if (pCurNode->count < M)
{
return;
}
//开始进行分裂
while (true)
{
//开始分裂成两个额外的节点
BTreeNode<T, M>* tmp1 = new BTreeNode<T, M>;
BTreeNode<T, M>* tmp2 = new BTreeNode<T, M>;
int midIdx = (int)(float(M) / 2) - 1;
//赋值左边节点数据
for (int i = 0; i < midIdx; ++i)
{
tmp1->data[tmp1->count++] = pCurNode->data[i];
}
//复制右边节点的数据
for (int i = midIdx + 1; i < pCurNode->count; ++i)
{
tmp2->data[tmp2->count++] = pCurNode->data[i];
}
//赋值节点
int index = 0;
for (int i = 0; i <= midIdx; ++i)
{
tmp1->childs[index++] = pCurNode->childs[i];
if (nullptr != pCurNode->childs[i])
{
pCurNode->childs[i]->parent = tmp1;
}
}
index = 0;
for (int i = midIdx + 1; i <= pCurNode->count; ++i)
{
tmp2->childs[index++] = pCurNode->childs[i];
if (nullptr != pCurNode->childs[i])
{
pCurNode->childs[i]->parent = tmp2;
}
}
pCurNode->data[0] = pCurNode->data[midIdx];
pCurNode->count = 1;
pCurNode->childs[0] = tmp1;
tmp1->parent = pCurNode;
pCurNode->childs[1] = tmp2;
tmp2->parent = pCurNode;
for (int i = 2; i < M; ++i)
{
pCurNode->childs[i] = nullptr;
}
for (int i = 1; i < M; ++i)
{
pCurNode->data[i] = -1;
}
//与父节点合并,注意这里的坐标调整
if (nullptr != pCurNode->parent)
{
BTreeNode<T, M>* parentNode = pCurNode->parent;
int index = 0;
while (index < parentNode->count)
{
if (parentNode->data[index] < pCurNode->data[0])
{
index++;
}
else
{
break;
}
}
//转移数据
for (int i = parentNode->count - 1; i >= index; --i)
{
parentNode->data[i + 1] = parentNode->data[i];
parentNode->childs[i + 1 + 1] = parentNode->childs[i + 1];
}
parentNode->data[index] = pCurNode->data[0];
parentNode->count++;
parentNode->childs[index] = tmp1;
tmp1->parent = parentNode;
parentNode->childs[index + 1] = tmp2;
tmp2->parent = parentNode;
if (parentNode->count < M)
{
break;
}
else
{
pCurNode = parentNode;
continue;
}
}
else
{
break;
}
}
}
template<typename T, int M>
void BTree<T, M>::InsertDataToNode(BTreeNode<T, M>*& Node, T e)
{
int index = 0;
for (index = Node->count - 1; index >= 0; index--)
{
if (Node->data[index] > e)
{
Node->data[index + 1] = Node->data[index];
}
else
{
break;
}
}
Node->data[index + 1] = e;
Node->count++;
}
template<typename T, int M>
void BTree<T, M>::DeleteElem(BTreeNode<T, M>*& Node, T e)
{
int FindIdx = -1;
BTreeNode<T, M>* pDeleteNode = SearchElem(e, FindIdx);
if (nullptr == pDeleteNode)
{
std::cout << std::endl << "Not Exist!" << std::endl;
return;
}
if (nullptr == pDeleteNode->parent && nullptr == pDeleteNode->childs[0])
{
return;
}
int MinCount = (int)((float)(M) / 2) - 1;
//判断该节点是否为叶子节点,非叶子节点要转成叶子节点
if (nullptr != pDeleteNode->childs[0])
{
BTreeNode<T, M>* leftChild = pDeleteNode->childs[FindIdx];
BTreeNode<T, M>* tmpNode = leftChild;
BTreeNode<T, M>* rightChild = nullptr;
//找到左孩子的最右边节点
while (nullptr != tmpNode)
{
rightChild = tmpNode;
tmpNode = tmpNode->childs[tmpNode->count];
}
pDeleteNode->data[FindIdx] = rightChild->data[rightChild->count - 1];
pDeleteNode = rightChild;
FindIdx = pDeleteNode->count - 1;
}
//删除该节点
DeleteDataFromNode(pDeleteNode, FindIdx);
//删除节点个数够,直接删除
if (pDeleteNode->count >= MinCount)
{
return;
}
//删除该节点后导致节点个数不满足B树的性质, 需要调整B树结构
int borrowIdx = -1; //借的兄弟节点的坐标
int childIdx = -1; //属于父亲节点中的位置
BTreeNode<T, M>* parentNode = pDeleteNode->parent;
BTreeNode<T, M>* borrowedNode = nullptr;
//获取子节点位于父节点的第几个孩子
for (int i = 0; i <= parentNode->count; ++i)
{
if (pDeleteNode == parentNode->childs[i])
{
childIdx = i;
break;
}
}
//优先向左兄弟借
borrowIdx = childIdx - 1;
if (borrowIdx >= 0 && parentNode->childs[borrowIdx]->count > MinCount)
{
//记录被借节点
borrowedNode = parentNode->childs[borrowIdx];
pDeleteNode->data[FindIdx] = parentNode->data[childIdx - 1];
pDeleteNode->count++; //借了一个节点后,数量加1
parentNode->data[childIdx - 1] = borrowedNode->data[(borrowedNode->count - 1)];
//从被借节点中删除该数据
DeleteDataFromNode(borrowedNode, (borrowedNode->count - 1));
return;
}
//左兄弟不够借,向右兄弟借
borrowIdx = childIdx + 1;
if (borrowIdx <= parentNode->count && parentNode->childs[borrowIdx]->count > MinCount)
{
//记录被借节点
borrowedNode = parentNode->childs[borrowIdx];
pDeleteNode->data[FindIdx] = parentNode->data[childIdx];
pDeleteNode->count++;
parentNode->data[childIdx] = borrowedNode->data[0];
//从被借节点中删除该数据
DeleteDataFromNode(borrowedNode, 0);
return;
}
BTreeNode<T, M>* pMergeNode = nullptr;
int indexMerge = -1;
//两个都不够借,需要进行合并
if (childIdx > 0)
{
indexMerge = childIdx - 1;
pMergeNode = parentNode->childs[indexMerge];
InsertDataToNode(pMergeNode, parentNode->data[childIdx - 1]);
DeleteDataFromNode(parentNode, childIdx - 1);
DeleteChildFromNode(parentNode, childIdx);
pDeleteNode = nullptr;
}
else
{
indexMerge = childIdx + 1;
pMergeNode = parentNode->childs[indexMerge];
InsertDataToNode(pMergeNode, parentNode->data[childIdx]);
DeleteDataFromNode(parentNode, childIdx);
DeleteChildFromNode(parentNode, childIdx);
pDeleteNode = nullptr;
}
//最后一种情况(待定)
if (parentNode->count > 0)
{
return;
}
parentNode->count = pMergeNode->count;
//拷贝数据
for (int i = 0; i < pMergeNode->count; ++i)
{
parentNode->data[i] = pMergeNode->data[i];
}
for (int i = 0; i <= M; ++i)
{
parentNode->childs[i] = nullptr;
}
delete pMergeNode;
pMergeNode = nullptr;
}
template<typename T, int M>
BTreeNode<T, M>* BTree<T, M>::SearchElem(BTreeNode<T, M>* Node, T e, int& FindIdx)
{
BTreeNode<T, M>* pCurNode = Node;
int index = 0;
while (nullptr != pCurNode)
{
if (pCurNode->data[index] == e)
{
FindIdx = index;
return pCurNode;
}
else if (pCurNode->data[index] < e && index < pCurNode->count)
{
++index;
}
else
{
pCurNode = pCurNode->childs[index];
index = 0;
}
}
FindIdx = -1;
return nullptr;
}
template<typename T, int M>
void BTree<T, M>::DeleteDataFromNode(BTreeNode<T, M>*& Node, int Index)
{
//注意这里节点的孩子为空
for (int i = Index; i < Node->count; ++i)
{
Node->data[i] = Node->data[i + 1];
}
Node->data[Node->count - 1] = -1;
Node->count--;
}
template<typename T, int M>
void BTree<T, M>::DeleteChildFromNode(BTreeNode<T, M>*& Node, int Index)
{
for (int i = Index; i <= Node->count; ++i)
{
Node->childs[i] = Node->childs[i + 1];
}
}
template<typename T, int M>
void BTree<T, M>::LevelTraverse(BTreeNode<T, M>* Node)
{
std::queue<BTreeNode<T, M>* > BTreeQueue;
BTreeQueue.push(Node);
int High = 0;
while (!BTreeQueue.empty())
{
BTreeNode<T, M>* node = BTreeQueue.front();
if (High != GetNodeHigh(node))
{
std::cout << std::endl;
High = GetNodeHigh(node);
}
BTreeQueue.pop();
for (int i = 0; i < node->count; ++i)
{
std::cout << node->data[i] << " ";
}
std::cout << "+++|";
for (int i = 0; i <= node->count; ++i)
{
if (nullptr != node->childs[i])
{
BTreeQueue.push(node->childs[i]);
}
}
}
}
template<typename T, int M>
int BTree<T, M>::GetNodeHigh(BTreeNode<T, M>* Node)
{
int high = 0;
while (nullptr != Node->parent)
{
high++;
Node = Node->parent;
}
return high;
}
#define random(a,b) (rand()%(b-a+1)+a)
int main()
{
BTree<int, 4> bTree;
//srand((unsigned)time(NULL));
int arr[100] = { 0 };
int count = 0;
for (int i = 0; i < 10; i++)
{
int num = random(1, 100);
bool same = false;
for (int j = 0; j < 20; ++j)
{
if (num == arr[j])
{
same = true;
break;
}
}
if (!same)
{
arr[count++] = num;
bTree.InsertElem(num);
}
else
{
}
}
bTree.LevelTraverse();
#if 0
cout << endl << "++++++++++++++++++++++" << endl;
int FindIndex = -1;
if (nullptr != bTree.SearchElem(96, FindIndex))
{
cout << "find!" << "Index = " << FindIndex;
}
else
{
cout << "not find!";
}
#endif // 0
#if 0
bTree.DeleteElem(96);
bTree.LevelTraverse();
bTree.DeleteElem(63);
bTree.LevelTraverse();
#endif // 0
bTree.DeleteElem(42);
bTree.LevelTraverse();
bTree.DeleteElem(35);
bTree.LevelTraverse();
bTree.DeleteElem(65);
bTree.LevelTraverse();
bTree.DeleteElem(1);
bTree.LevelTraverse();
bTree.DeleteElem(63);
bTree.LevelTraverse();
bTree.DeleteElem(25);
bTree.LevelTraverse();
bTree.DeleteElem(59);
bTree.LevelTraverse();
bTree.DeleteElem(68);
bTree.LevelTraverse();
bTree.InsertElem(20);
bTree.LevelTraverse();
bTree.InsertElem(40);
bTree.LevelTraverse();
return 0;
}
2.2、B+树
B+树是在B树的基础上进行了优化。
(1)分支节点的子树指针与关键字个数相同。(就相当于是取消掉了原先B树每个结点的最左边的那个孩子)。
(2)分支节点的子树指针p[i]指向关键字值大小在[k[i],k[i+1])区间之间。
(3)所有叶子节点增加一个链接指针链接在一起。
(4)所有关键字及其映射数据都在叶子节点出现。
注意:
(1)分支节点跟叶子结点有重复的值,分支节点存的是叶子结点的索引。
(2)父亲中存的是孩子结点中的最小值做索引。
(3)分支节点可以只存key,叶子结点存key/value。
插入操作:图示:以5阶B+树举例进行插入,根据B+树的定义,结点最多有4个值,最少有2个值:
(1)空树插入5、8、10、15;
(2)插入16;
超过了最大值4,所以分裂,以中间为准;
(3)插入17、18;
删除操作:图示:
(1)原始状态:
(2)删除22,删除后个数为2,删除结束;
(3)删除15;
删除之后,只有一个值,而兄弟有三个值,所以从兄弟结点借一个关键字,并更新索引结点。
示例:
2.2、红黑树
2.3、哈希
3、设计模式
详细见【设计模式】专栏
4、C++库
详细见【C++ STL】专栏
5、Linux工程
详细见【Linux】专栏
6、网络编程
6.1、Linux IO
I/O(input/output)也就是输入和输出,在冯诺依曼体系结构当中,将数据从输入设备拷贝到内存就叫作输入,将数据从内存拷贝到输出设备就叫作输出。
IO 最主要的问题就是效率问题,以读取数据为例:
当 read/recv 时,如果底层缓冲区中没有数据,read/recv 就会阻塞等待。
当 read/recv 时,如果底层缓冲区中有数据,read/recv 就会进行拷贝。
所以,IO 的本质就是:等待(等待 IO 条件就绪) + 数据拷贝(当 IO 条件就绪后将数据拷贝到内存或外设)。只要缓冲区中没有数据,read/recv 就会一直阻塞等待,直到缓冲区中出现数据,然后进行拷贝,所以 read/recv 就会花费大量时间在等这一操作上面,这就是一种低效的 IO 模式。
任何 IO 的过程,都包含 “等” 和 “拷贝” 这两个步骤,但在实际的应用场景中 “等” 消耗的时间往往比 “拷贝” 消耗的时间多。
五种IO模型:
(1)阻塞式IO【用1杆鱼竿钓鱼,将鱼钩抛入水中后,就一直盯着浮标一动不动,不理会外界的任何动静,直到有鱼上钩,就挥动鱼竿将鱼钓上来】。
(2)非阻塞轮询式IO【用1杆鱼竿钓鱼,将鱼钩抛入水中后,就可以去做其它事情了,然后定期观察浮标的动静,如果有鱼上钩就将鱼钓上来,否则就继续做其它事情】。
(3)信号驱动IO【用1杆鱼竿钓鱼,将鱼钩抛入水中后,在鱼竿顶部绑一个铃铛,就可以去做其它事情了,如果铃铛一响就知道有鱼上钩了,于是挥动鱼竿将鱼钓上来,否则就不管鱼竿。】。
(4)多路复用,多路转接【用100杆鱼竿钓鱼,将 100 个鱼钩抛入水中后,就定期观察这 100 个浮漂的动静,如果某个鱼竿有鱼上钩就挥动对应的鱼竿将鱼钓上来。】。
(5)异步IO【我是公司CEO,我不钓鱼,但我会告诉老墨,我想吃鱼了,所以老墨拿着桶、鱼竿、电话去钓鱼。鱼钓上后,打电话通知我。】。
6.2、阻塞IO
阻塞I/O(Blocking I/O)是一种最常见的I/O模型,它是默认的I/O操作方式。在这种模式下,当一个进程对某个文件描述符(比如网络套接字、磁盘文件等)执行读或写操作时,如果该操作不能立即完成(例如,数据还没有到达、缓冲区满等),则进程会被挂起,直到数据准备好或者操作可以执行为止。这种挂起状态会导致进程暂停执行,直到I/O操作完成。
特点:
(1)简单性:编程模型简单,易于理解和实现。
(2)资源利用率:在单个I/O密集型应用中,CPU的利用率可能较低,因为进程大部分时间都在等待I/O操作完成。
(3)并发性:对于需要处理大量并发连接的服务器应用来说,可能需要为每个连接分配一个单独的线程或进程,这会导致大量的上下文切换和较高的内存消耗。
/*
在基于TCP的网络编程中,服务器通常会接受客户端的连接请求,并在每个连接上接收和发送数据。使用阻塞I/O
*/
socket = listen(port); // 监听端口
while (true) {
client_socket = accept(socket); // 等待并接受客户端连接
while (true) {
data = read(client_socket); // 读取客户端数据,如果数据没有到达,则阻塞
// 处理数据...
write(client_socket, response); // 发送响应到客户端
}
close(client_socket);
}
6.3、非阻塞轮询式IO
6.4、
6.5、
6.6、
6.7、
6.7.1、select
select 是系统提供的一个多路转接的接口,可以用来实现多路复用输入 / 输出模型。select 系统调用可以让程序同时监视多个文件描述符上的状态变化。
select 核心工作就是等,当监视的文件描述符中有一个或多个事件就绪时,也就是直到被监视的文件描述符有一个或多个发生了状态改变,select 才会成功返回并将对应文件描述符的就绪事件告知调用者。
6.7.1.1、函数原型
函数原型:
#include <sys/time.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
nfds 是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错。在linux系统中,select的默认最大值为1024。设置这个值的目的是为了不用每次都去轮询这1024个fd,假设我们只需要几个套接字,我们就可以用最大的那个套接字的值加上1作为这个参数的值,当我们在等待是否有套接字准备就绪时,只需要监测maxfd+1个套接字就可以了,这样可以减少轮询时间以及系统的开销。
readfds 输入输出型参数,readfs是一个容器,里面可以容纳多个文件描述符,把需要监视的描述符放入这个集合中,当有文件描述符可读时,select就会返回一个大于0的值,表示有文件可读.
writefds 输入输出型参数,和readfs类似,表示有一个可写的文件描述符集合,当有文件可写时,select就会返回一个大于0的值,表示有文件可写。
exceptfds 输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已就绪。(这个参数使用一次过后,需要进行重新设定)
timeout 输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间。
参数timeout的取值:
NULL/nullptr select调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪。
0 select调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,select 检测后都会立即返回。
特定的时间值 select调用后在指定的时间内进行阻塞等待,若被监视的文件描述符上一直没有事件就绪,则在该时间后select进行超时返回。
返回值说明:
若函数调用成功,则返回事件就绪的文件描述符个数。
若timeout时间耗尽,则返回0。
若函数调用失败,则返回-1,同时错误码被设置。
只要有一个fd数据就绪或空间就绪,就可以进行返回了。
错误码:
select调用失败时,错误码可能被设置为:
EBADF 文件描述符为无效的或该文件已关闭。
EINTR 此调用被信号所中断。
EINVAL 参数nfds为负值。
ENOMEM 核心内存不足。
数据结构fd_set:
其实这个结构就是一个整数数组,更严格的说 fd_set 本质也是一个位图,用位图中对应的位来表示要监视的文件描述符。
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
数据结构timeval:
在Linux的C语言标准库中,timeval结构体的定义通常位于<sys/time.h>头文件中。
格式如下:
struct timeval {
time_t tv_sec; // 秒
suseconds_t tv_usec; // 微秒
};
gettimeofday() 该函数用于获取当前时间(包括秒和微秒),并将结果存储在timeval结构体中。
settimeofday() 该函数用于设置系统时间,虽然它也接受一个timeval结构体作为参数,但通常用于系统时间校准等高级场景。
/*
fs_set的一些操作
*/
#include <stdio.h>
#include <sys/select.h>
#include <unistd.h>
int main(void) {
fd_set fdset;
FD_ZERO(&fdset); /* 清空集合中所有的元素 */
FD_SET(STDOUT_FILENO, &fdset); /* 将stdout的文件描述符添加到集合中 */
if (FD_ISSET(STDOUT_FILENO, &fdset) != 0) /* 测试stdout是否包含在集合中 */
printf("stdout has been set\n");
else
printf("stdout has not been set\n");
FD_CLR(STDOUT_FILENO, &fdset); /* 从集合中移除stdout的文件描述符 */
if (FD_ISSET(STDOUT_FILENO, &fdset) != 0)
printf("stdout has been set\n");
else
printf("stdout has not been set\n");
return 0;
}
6.7.1.2、工作流程
如果要实现一个简单的select服务器,读取客户端发来的数据并进行打印,工作流程如下:
(1)先初始化服务器,完成套接字的创建、绑定和监听。
(2)定义一个_fd_array数组用于保存监听套接字和已经与客户端建立连接的套接字,初始化时就可将监听套接字添加到_fd_array数组中。
(3)然后服务器开始循环调用select函数,检测读事件是否就绪,若就绪则执行对应操作。
(4)每次调用select函数之前,都需要定义一个读文件描述符集readfds,并将_fd_array中的文件描述符依次设置进readfds中,表示让select监视这些文件描述符的读事件是否就绪。
(5)当select检测到数据就绪时会将读事件就绪的文件描述符设置进readfds中,此时就能够得知哪些文件描述符的读事件就绪,并对这些文件描述符进行对应操作。
(6)若读事件就绪的是监听套接字,则调用accept函数从底层全连接队列获取已建立的连接,并将该连接对应的套接字添加到_fd_array数组中。
(7)若读事件就绪的是与客户端建立连接的套接字,则调用read函数读取客户端发来的数据并进行打印输出。
(8)服务器与客户端建立连接的套接字读事件就绪,也可能是客户端将连接关闭了,此时服务器应该调用close关闭该套接字,并将该套接字从_fd_array数组中清除,不需要再监视该文件描述符的读事件了。
6.7.1.3、示例
服务器:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define LISTEN_BACKLOG (5)
#define BUF_SIZE (2048)
#define ONCE_READ_SIZE (1500)
#define EPOLL_SIZE (100);
#define MAX_EVENTS (10)
void usage(void) {
printf("*********************************\n");
printf("./server 本端ip 本端端口\n");
printf("*********************************\n");
}
// 当前已有的链接
#define MAXCLINE 5 // 连接队列中的个数
int fd[MAXCLINE]; // 连接的fd
int conn_amount; // 此次while循环,有发送过来数据的套接字个数
int main(int argc, char *argv[])
{
struct sockaddr_in local; // 服务器端地址
struct sockaddr_in peer; // 客户端地址
socklen_t addrlen = sizeof(peer);
int sock_fd = 0; // 服务器端套接字
int new_fd = 0; // 客户端套接字
int ret = 0;
char send_buf[BUF_SIZE] = {0}; // 发送缓冲区
char recv_buf[BUF_SIZE] = {0}; // 接收缓冲区
if (argc != 3) {
usage();
return -1;
}
char *ip = argv[1];
unsigned short port = atoi(argv[2]);
printf("ip:port->%s:%u\n", argv[1], port);
// 创建服务器端套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
printf("socket: %d\n", sock_fd);
if (sock_fd == -1) {
perror("socket error");
return -1;
}
// 初始化服务端地址结构体 包含ip地址、端口号
memset(&local, 0, sizeof(struct sockaddr_in));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(ip);
local.sin_port = htons(port);
// 绑定端口 绑定套接字、ip地址、端口号之间的关系
ret = bind(sock_fd, (struct sockaddr *)&local, sizeof(struct sockaddr));
if (ret == -1) {
close(sock_fd);
perror("bind error");
return -1;
}
// 监听服务器套接字
ret = listen(sock_fd, LISTEN_BACKLOG);
if (ret == -1) {
close(sock_fd);
perror("listen error");
return -1;
}
//文件描述符集合的定义
fd_set fdsr;
int maxsock = sock_fd;
struct timeval tv;
while (1) {
// 清空 文件描述符集合
FD_ZERO(&fdsr);
// 将 服务器套接字 添加到 文件描述符集合 中
FD_SET(sock_fd, &fdsr);
//超时的设定,这里也可以不需要设置时间,将这个参数设置为NULL, 表明此时select为阻塞模式
tv.tv_sec = 30;
tv.tv_usec =0;
//将当前已有的连接全部加到这个这个集合中,可以监测客户端是否有数据到来
for(int i = 0; i < MAXCLINE; i++)
{
if(fd[i]!=0)
{
FD_SET(fd[i], &fdsr);
}
}
// 等待事件发生
ret = select(maxsock + 1, &fdsr, NULL, NULL, &tv);
if(ret < 0)
{
// 没有找到有效的连接 失败
perror("select error!\n");
break;
} else if(ret == 0)
{
// 指定的时间到,
printf("timeout \n");
continue;
}
// 接受数据 遍历【必要流程】
for(int i =0; i < conn_amount; i++)
{
// 该套接字在集合中
if(FD_ISSET(fd[i], &fdsr))
{
ret = recv(fd[i], recv_buf, sizeof(recv_buf), 0);
if(ret <= 0) // 客户端连接关闭,清除文件描述符集中的相应的位
{
printf("client[%d] close\n", i);
close(fd[i]);
FD_CLR(fd[i], &fdsr);
fd[i] = 0;
conn_amount--;
} else {
// 否则有相应的数据发送过来 ,进行相应的处理
if(ret < BUF_SIZE)
memset(&recv_buf[ret], '\0', 1);
printf("client[%d] send:%s\n", i, recv_buf);
}
}
}
// 循环遍历所有的连接,检测是否有数据到来【必要流程】
if(FD_ISSET(sock_fd, &fdsr))
{
new_fd = accept(sock_fd, (struct sockaddr *)&peer, &addrlen);
if(new_fd <= 0)
{
perror("accept error\n");
continue;
}
//添加新的fd到数组中 判断有效的连接数是否小于最大的连接数,如果小于的话,就把新的连接套接字加入集合
if(conn_amount < MAXCLINE)
{
for(int i = 0; i < MAXCLINE; i++)
{
if(fd[i]==0)
{
fd[i] = new_fd;
break;
}
}
conn_amount++;
printf("new connection client[%d]%s:%d\n", conn_amount, inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
if(new_fd > maxsock)
{
maxsock = new_fd;
}
} else {
printf("max connections arrive ,exit\n");
send(new_fd,"bye",4,0);
close(new_fd);
continue;
}
}
}
return 0;
}
[root@aaabbb]#
[root@aaabbb]# ./socket-select-server 127.0.0.1 4000
ip:port->127.0.0.1:4000
socket: 3
new connection client[1]127.0.0.1:47328
client[0] send:1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
client[0] send:2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
client[0] send:3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
client[0] send:4444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444
client[0] send:5555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555
client[0] send:6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666
client[0] send:7777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777
client[0] send:8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888
client[0] send:9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
client[0] send:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
client[0] send:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
client[0] send:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
[root@aaabbb]#
[root@aaabbb]#
客户端:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define LISTEN_BACKLOG (5)
#define BUF_SIZE (100)
#define REQUEST_STR "tcp pack"
void usage(void) {
printf("*********************************\n");
printf("./client 对端ip 对端端口\n");
printf("*********************************\n");
}
int main(int argc, char *argv[])
{
struct sockaddr_in client; // 客户端地址
struct sockaddr_in server; // 服务器地址
int sock_fd = 0;
int ret = 0;
socklen_t addrlen = 0;
char send_buf[BUF_SIZE] = {0};
char recv_buf[BUF_SIZE] = {0};
if (argc != 3) {
usage();
return -1;
}
char *ip = argv[1];
unsigned short port = atoi(argv[2]);
printf("ip:port->%s:%u\n", argv[1], port);
// 创建客户端套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket error");
return -1;
}
printf("sock_fd: %d\n", sock_fd);
memset(&server, 0, sizeof(struct sockaddr_in));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip);
server.sin_port = htons(port);
ret = connect(sock_fd, (struct sockaddr *)&server, sizeof(struct sockaddr));
if (ret == -1) {
close(sock_fd);
perror("connect error");
return -1;
}
char seq = 0x31;
while(1) {
memset(send_buf, seq, BUF_SIZE);
send(sock_fd, send_buf, BUF_SIZE, 0);
printf("send %s\n", send_buf);
sleep(2);
seq++;
}
close(sock_fd);
return 0;
}
[root@aaabbb]#
[root@aaabbb]# ./socket-epoll-client 127.0.0.1 4000
ip:port->127.0.0.1:4000
sock_fd: 3
send 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
send 2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
send 3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
send 4444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444
send 5555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555
send 6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666
send 7777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777
send 8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888
send 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
send ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
send ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
send <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
send ====================================================================================================
[root@aaabbb]#
[root@aaabbb]#
6.7.2、poll
poll也是一种linux中多路转接的方案。它所对应的多路转接方案主要是解决select存在的两个问题:select的文件描述符有上限的问题;select每次都要重新设置关心的fd问题。
6.7.2.1、函数原型
函数原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数说明:
struct pollfd *fds 指向pollfd结构体数组的指针,每个pollfd结构体代表一个要监视的文件描述符。该参数是待监控事件的集合。
nfds_t nfds fds数组中pollfd结构体的数量。这个值必须是正数。
int timeout 等待事件发生前的超时时间(毫秒)。如果timeout为-1,则poll将无限期地等待直到至少有一个文件描述符就绪。如果timeout为0,则poll将立即返回,不阻塞。
返回值说明:
成功时,poll返回准备就绪的文件描述符的数量(即fds数组中revents字段非零的pollfd结构体的数量)。
如果在调用时发生错误,则返回-1,并设置errno以指示错误的原因。
结构体struct pollfd说明:
struct pollfd {
int fd 要监视的文件描述符。
short events 请求监视的事件类型,可以是POLLIN(可读)、POLLOUT(可写)、POLLERR(错误)、POLLHUP(挂起)、POLLNVAL(无效请求)等,可以使用位或(|)操作符组合多个事件。
short revents:在调用返回时,包含实际发生的事件的位掩码。这个字段在调用poll之前应该被忽略。
short revents poll函数返回时告知用户该文件描述符上的哪些事件已经就绪。
};
events和revents事件类型有如下种类:
#define POLLIN 0x0001 //有数据需要读取
#define POLLPRI 0x0002 //
#define POLLOUT 0x0004 //可以写入数据
#define POLLERR 0x0008 //调用出错,仅在revents中出现
#define POLLHUP 0x0010 //调用中断,仅在revents中出现
#define POLLNVAL 0x0020 //无效的请求,仅在revents中出现
6.7.2.2、示例
服务器:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>
#include <sys/poll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define LISTEN_BACKLOG (5)
#define BUF_SIZE (2048)
#define ONCE_READ_SIZE (1500)
#define EPOLL_SIZE (100);
#define MAX_EVENTS (10)
void usage(void) {
printf("*********************************\n");
printf("./server 本端ip 本端端口\n");
printf("*********************************\n");
}
// 当前已有的链接
#define MAXCLINE 5 // 连接队列中的个数
int fd[MAXCLINE]; // 连接的fd
int conn_amount; // 此次while循环,有发送过来数据的套接字个数
int main(int argc, char *argv[])
{
struct sockaddr_in local; // 服务器端地址
struct sockaddr_in peer; // 客户端地址
socklen_t addrlen = sizeof(peer);
int sock_fd = 0; // 服务器端套接字
int new_fd = 0; // 客户端套接字
int ret = 0;
char send_buf[BUF_SIZE] = {0}; // 发送缓冲区
char recv_buf[BUF_SIZE] = {0}; // 接收缓冲区
if (argc != 3) {
usage();
return -1;
}
char *ip = argv[1];
unsigned short port = atoi(argv[2]);
printf("ip:port->%s:%u\n", argv[1], port);
// 创建服务器端套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
printf("socket: %d\n", sock_fd);
if (sock_fd == -1) {
perror("socket error");
return -1;
}
// 初始化服务端地址结构体 包含ip地址、端口号
memset(&local, 0, sizeof(struct sockaddr_in));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(ip);
local.sin_port = htons(port);
// 绑定端口 绑定套接字、ip地址、端口号之间的关系
ret = bind(sock_fd, (struct sockaddr *)&local, sizeof(struct sockaddr));
if (ret == -1) {
close(sock_fd);
perror("bind error");
return -1;
}
// 监听服务器套接字
ret = listen(sock_fd, LISTEN_BACKLOG);
if (ret == -1) {
close(sock_fd);
perror("listen error");
return -1;
}
//文件描述符集合的定义
/*
poll只是对select函数传入的参数的一个封装,底层实现还是select,好处就是需要传入的参数少了
*/
// pollfd数组, 一个元素为一个 描述符以及相对应的事件。元素的个数为可处理socket的最大个数,突破了select的1024的限制
pollfd pfds[2048] = {0};
pfds[sock_fd].fd = sock_fd;
pfds[sock_fd].events = POLLIN; // 可读时间
int maxsock = sock_fd;
//将当前已有的连接全部加到这个这个集合中,可以监测客户端是否有数据到来
for(int i = 0; i < MAXCLINE; i++)
{
if(fd[i] != 0)
{
pfds[fd[i]].fd = fd[i];
pfds[fd[i]].events = POLLIN;
maxsock = maxsock > fd[i] ? maxsock : fd[i];
}
}
struct timeval tv;
while (1) {
// 等待事件发生
ret = poll(pfds, maxsock + 1, -1);
// 循环遍历所有的连接,检测是否有数据到来【必要流程】
for (int i = 1; i <= maxsock; i++)
{
if (pfds[i].revents & POLLIN)
{
int clientfd = accept(pfds[i].fd, (sockaddr*)&peer, &addrlen);
printf("accept client[%d %d]\n", i, clientfd);
if (clientfd < 0)
{
continue;
}
pfds[clientfd].fd = clientfd;
pfds[clientfd].events = POLLIN;
maxsock = maxsock > clientfd ? maxsock : clientfd;
}
}
// 接受数据 遍历【必要流程】
for (int i = 1; i <= maxsock; i++)
{
if (pfds[i].revents & POLLIN)
{
ret = recv(pfds[i].fd, recv_buf, sizeof(recv_buf), 0);
if(ret <= 0) // 客户端连接关闭,清除文件描述符集中的相应的位
{
printf("client[%d] close\n", i);
close(fd[i]);
pfds[i].fd = 0;
pfds[i].events = 0;
continue;
} else {
// 否则有相应的数据发送过来 ,进行相应的处理
if(ret < BUF_SIZE)
memset(&recv_buf[ret], '\0', 1);
printf("client[%d] send:%s\n", i, recv_buf);
}
}
}
}
return 0;
}
[root@aaabbb]#
[root@aaabbb]# ./socket-poll-server 127.0.0.1 3000
ip:port->127.0.0.1:3000
socket: 3
accept client[3 4]
client[3] close
accept client[4 -1]
client[4] send:1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
accept client[4 -1]
client[4] send:2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
accept client[4 -1]
client[4] send:3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
accept client[4 -1]
client[4] send:4444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444
accept client[4 -1]
client[4] send:5555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555
accept client[4 -1]
client[4] send:6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666
accept client[4 -1]
client[4] send:7777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777
accept client[4 -1]
client[4] send:8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888
accept client[4 -1]
client[4] send:9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
accept client[4 -1]
client[4] send:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
accept client[4 -1]
client[4] send:;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
accept client[4 -1]
client[4] send:<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
accept client[4 -1]
client[4] send:====================================================================================================
accept client[4 -1]
client[4] send:>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
accept client[4 -1]
client[4] send:????????????????????????????????????????????????????????????????????????????????????????????????????
accept client[4 -1]
client[4] send:@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
^Xaccept client[4 -1]
client[4] send:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
accept client[4 -1]
client[4] send:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
^Caccept client[4 -1]
client[4] send:CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
accept client[4 -1]
client[4] send:DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
accept client[4 -1]
client[4] close
[root@aaabbb]#
[root@aaabbb]#
客户端:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define LISTEN_BACKLOG (5)
#define BUF_SIZE (100)
#define REQUEST_STR "tcp pack"
void usage(void) {
printf("*********************************\n");
printf("./client 对端ip 对端端口\n");
printf("*********************************\n");
}
int main(int argc, char *argv[])
{
struct sockaddr_in client; // 客户端地址
struct sockaddr_in server; // 服务器地址
int sock_fd = 0;
int ret = 0;
socklen_t addrlen = 0;
char send_buf[BUF_SIZE] = {0};
char recv_buf[BUF_SIZE] = {0};
if (argc != 3) {
usage();
return -1;
}
char *ip = argv[1];
unsigned short port = atoi(argv[2]);
printf("ip:port->%s:%u\n", argv[1], port);
// 创建客户端套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket error");
return -1;
}
printf("sock_fd: %d\n", sock_fd);
memset(&server, 0, sizeof(struct sockaddr_in));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip);
server.sin_port = htons(port);
ret = connect(sock_fd, (struct sockaddr *)&server, sizeof(struct sockaddr));
if (ret == -1) {
close(sock_fd);
perror("connect error");
return -1;
}
char seq = 0x31;
while(1) {
memset(send_buf, seq, BUF_SIZE);
send(sock_fd, send_buf, BUF_SIZE, 0);
printf("send %s\n", send_buf);
sleep(2);
seq++;
}
close(sock_fd);
return 0;
}
[root@aaabbb]#
[root@aaabbb]# ./socket-epoll-client 127.0.0.1 3000
ip:port->127.0.0.1:3000
sock_fd: 3
send 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
send 2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
send 3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
send 4444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444
send 5555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555
send 6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666
send 7777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777
send 8888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888
send 9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
send ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
send ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
send <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
send ====================================================================================================
send >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
send ????????????????????????????????????????????????????????????????????????????????????????????????????
send @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
send AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
send BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
send CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
send DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD
[root@aaabbb]#
[root@aaabbb]#
6.7.3、epoll【*****】
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
优势:
(1)无限制的文件描述符数量:epoll所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,在1GB内存的机器上大约是10万左右,具体数目可以通过cat /proc/sys/fs/file-max
查看。
(2)高效的IO事件处理:epoll通过内核与用户空间共享一个事件表,当文件描述符的状态发生变化时,内核会将这个事件通知给用户空间,用户空间再根据事件类型进行相应的处理。这种方式避免了select/poll的轮询机制,提高了效率。
(3)支持两种触发模式:epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
6.7.3.1、函数原型
函数原型:
(1) 创建一个epoll实例,并返回一个文件描述符,用于后续的epoll操作。这个文件描述符用于引用epoll实例
#include <sys/epoll.h>
int epoll_create(int size);
参数说明:
size 表示内核需要监控的最大数量。然而,这个参数在现代Linux内核中已经被忽略,只需要传入一个大于0的值即可。
返回值说明:
成功时返回一个新的文件描述符。
失败时返回-1,并设置errno以指示错误。
(2) 用于向epoll实例中注册、修改或删除一个文件描述符(如socket)及其事件。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数说明:
epfd 由epoll_create返回的文件描述符。
op 指定要执行的操作,常见的操作有EPOLL_CTL_ADD(添加)、EPOLL_CTL_DEL(删除)和EPOLL_CTL_MOD(修改)。
fd 要注册、修改或删除的文件描述符。
event 指向epoll_event结构体的指针,用于指定事件类型和用户数据。
返回值说明:
成功时返回0。
失败时返回-1,并设置errno以指示错误。
结构体struct epoll_event说明:
#include <sys/epoll.h>
struct epoll_event{
uint32_t events; // epoll事件,参考事件列表
epoll_data_t data;
} ;
typedef union epoll_data {
void *ptr;
int fd; // 套接字文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll事件说明:
#include <sys/epoll.h>
enum EPOLL_EVENTS
{
EPOLLIN = 0x001, //读事件
EPOLLPRI = 0x002,
EPOLLOUT = 0x004, //写事件
EPOLLRDNORM = 0x040,
EPOLLRDBAND = 0x080,
EPOLLWRNORM = 0x100,
EPOLLWRBAND = 0x200,
EPOLLMSG = 0x400,
EPOLLERR = 0x008, //出错事件
EPOLLHUP = 0x010, //出错事件
EPOLLRDHUP = 0x2000,
EPOLLEXCLUSIVE = 1u << 28,
EPOLLWAKEUP = 1u << 29,
EPOLLONESHOT = 1u << 30,
EPOLLET = 1u << 31 //边缘触发
};
(3) 等待注册在epoll实例上的文件描述符上的事件发生。如果事件发生,则将其复制到用户提供的数组中。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数说明:
epfd 由epoll_create返回的文件描述符。
events 指向epoll_event结构体的数组的指针,用于接收发生的事件。【传入内容是空,传出不为空】
maxevents 指定events数组的大小,即epoll_wait一次能处理的最大事件数。
timeout 等待事件发生前的超时时间(毫秒)。小于0:一直等待;等于0:立即返回;大于0:等待超时时间返回,单位毫秒。
返回值说明:
成功时返回发生事件的文件描述符的数量。
如果没有事件发生且没有超时,则返回0。
失败时返回-1,并设置errno以指示错误。
6.7.3.2、工作原理
(1)水平触发(LT)
工作原理:
在水平触发模式下,当某个文件描述符上有新的数据可读或可写时,epoll_wait会立即返回并通知应用程序。即使应用程序没有处理完所有的数据,下一次epoll_wait调用仍然会返回该文件描述符上的事件。
对于读操作:如果文件描述符上的接收缓冲区中有任何数据可读(不为空),epoll_wait会返回该文件描述符可读的事件。即使应用程序没有读取所有数据,下一次epoll_wait调用仍然会返回相同的可读事件。
对于写操作:如果文件描述符上的发送缓冲区有足够的空间可以写入数据,epoll_wait会返回该文件描述符可写的事件。
适用场景:
水平触发模式适用于典型的轮询方式,应用程序可以重复调用epoll_wait来处理文件描述符上的I/O事件,直到所有事件都被处理完毕。这种模式对于实时性要求不是非常高的应用,如普通的网络服务器或需要周期性处理数据的情况较为适用。
(2)边缘触发(ET)
工作原理:
在边缘触发模式下,epoll_wait只在文件描述符状态发生变化时才返回,并且只通知应用程序一次。也就是说,只有当文件描述符从无事件变为有事件时,epoll_wait才会返回。
对于读操作:仅当文件描述符上的接收缓冲区由空变为非空时,epoll_wait才会返回该文件描述符可读的事件。
对于写操作:仅当文件描述符上的发送缓冲区由满变为非满时,epoll_wait才会返回该文件描述符可写的事件。
边缘触发模式要求应用程序在接收到事件后立即处理所有可用的数据,因为下一次epoll_wait调用不会返回相同的事件。
适用场景:
边缘触发模式特别适合处理大量事件和高并发的场景。由于它只在状态变化时通知应用程序,可以减少不必要的上下文切换,提高效率。对于事件响应速度要求较高的应用,如高性能网络服务器,需要快速处理大量连接或数据的情况,边缘触发模式更为适合。
6.7.3.3、示例
服务器:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <stdbool.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define LISTEN_BACKLOG (5)
#define BUF_SIZE (2048)
#define ONCE_READ_SIZE (1500)
#define EPOLL_SIZE (100);
#define MAX_EVENTS (10)
void usage(void) {
printf("*********************************\n");
printf("./server 本端ip 本端端口\n");
printf("*********************************\n");
}
void setnonblocking(int fd) {
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
int main(int argc, char *argv[])
{
struct sockaddr_in local; // 服务器端地址
struct sockaddr_in peer; // 客户端地址
socklen_t addrlen = sizeof(peer);
int sock_fd = 0; // 服务器端套接字
int new_fd = 0; // 客户端套接字
int ret = 0;
char send_buf[BUF_SIZE] = {0}; // 发送缓冲区
char recv_buf[BUF_SIZE] = {0}; // 接收缓冲区
if (argc != 3) {
usage();
return -1;
}
char *ip = argv[1];
unsigned short port = atoi(argv[2]);
printf("ip:port->%s:%u\n", argv[1], port);
// 创建服务器端套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
printf("socket: %d\n", sock_fd);
if (sock_fd == -1) {
perror("socket error");
return -1;
}
// 初始化服务端地址结构体 包含ip地址、端口号
memset(&local, 0, sizeof(struct sockaddr_in));
local.sin_family = AF_INET;
local.sin_addr.s_addr = inet_addr(ip);
local.sin_port = htons(port);
// 绑定端口 绑定套接字、ip地址、端口号之间的关系
ret = bind(sock_fd, (struct sockaddr *)&local, sizeof(struct sockaddr));
if (ret == -1) {
close(sock_fd);
perror("bind error");
return -1;
}
// 监听服务器套接字
ret = listen(sock_fd, LISTEN_BACKLOG);
if (ret == -1) {
close(sock_fd);
perror("listen error");
return -1;
}
// 创建epoll实例
int epoll_size = EPOLL_SIZE;
int efd = epoll_create(epoll_size);
if (efd == -1) {
perror("epoll create error");
return -1;
}
// 定义 epoll文件描述符
struct epoll_event ev;
// 将服务器套接字添加到epoll监控列表中
ev.data.fd = sock_fd;
ev.events = EPOLLIN;
if (epoll_ctl(efd, EPOLL_CTL_ADD, sock_fd, &ev) == -1) {
perror("epoll ctl ADD error");
return -1;
}
// 定义 epoll事件结构体数组
struct epoll_event events[MAX_EVENTS];
int timeout = 1000;
while (1) {
// 等待epoll事件
int nfds = epoll_wait(efd, events, MAX_EVENTS, timeout);
if (nfds == -1) {
perror("epoll wait error");
return -1;
} else if (nfds == 0) {
printf("epoll wait timeout\n");
continue;
} else {
}
// 遍历所有触发的epoll事件
for (int i = 0; i < nfds; i++) {
int fd = events[i].data.fd;
printf("events[%d] events:%08x %d %d\n", i, events[i].events, fd, sock_fd);
if (fd == sock_fd) {
// 处理服务器套接字上的事件,即新的客户端连接
new_fd = accept(sock_fd, (struct sockaddr *)&peer, &addrlen);
if (new_fd == -1) {
perror("accept error");
continue;
}
setnonblocking(new_fd);
ev.data.fd = new_fd;
ev.events = EPOLLIN|EPOLLET;
if (epoll_ctl(efd, EPOLL_CTL_ADD, new_fd, &ev) == -1) {
perror("epoll ctl ADD new fd error");
close(new_fd);
continue;
}
} else {
// 处理客户端套接字上的事件,即客户端发送的数据
if (events[i].events & EPOLLIN) {
printf("fd:%d is readable\n", fd);
memset(recv_buf, 0, BUF_SIZE);
unsigned int len = 0;
while(1) {
ret = recv(fd, recv_buf + len, ONCE_READ_SIZE, 0);
if (ret == 0) {
printf("remove fd:%d\n", fd);
epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
} else if ((ret == -1) && ((errno == EINTR) || (errno == EAGAIN) || (errno == EWOULDBLOCK))) {
printf("fd:%d recv errno:%d done\n", fd, errno);
break;
} else if ((ret == -1) && !((errno == EINTR) || (errno == EAGAIN) || (errno == EWOULDBLOCK))) {
printf("remove fd:%d errno:%d\n", fd, errno);
epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
} else {
printf("once read ret:%d\n", ret);
len += ret;
}
}
printf("recv fd:%d, len:%d, %s\n", fd, len, recv_buf);
} if (events[i].events & EPOLLOUT) {
printf("fd:%d is sendable\n", fd);
} else if ((events[i].events & EPOLLERR) ||
((events[i].events & EPOLLHUP))) {
printf("fd:%d error\n", fd);
}
}
}
}
return 0;
}
[root@aaabbb]#
[root@aaabbb]# ./socket-epoll-server 127.0.0.1 5000
ip:port->127.0.0.1:5000
epoll wait timeout
epoll wait timeout
epoll wait timeout
events[0] events:00000001 3 3
events[0] events:00000001 5 3
fd:5 is readable
once read ret:100
fd:5 recv errno:11 done
recv fd:5, len:100, 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
epoll wait timeout
events[0] events:00000001 5 3
fd:5 is readable
once read ret:100
fd:5 recv errno:11 done
recv fd:5, len:100, 2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
epoll wait timeout
events[0] events:00000001 5 3
fd:5 is readable
once read ret:100
fd:5 recv errno:11 done
recv fd:5, len:100, 3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
epoll wait timeout
events[0] events:00000001 5 3
fd:5 is readable
once read ret:100
fd:5 recv errno:11 done
recv fd:5, len:100, 4444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444
epoll wait timeout
events[0] events:00000001 5 3
fd:5 is readable
once read ret:100
fd:5 recv errno:11 done
recv fd:5, len:100, 5555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555
epoll wait timeout
events[0] events:00000001 5 3
fd:5 is readable
once read ret:100
fd:5 recv errno:11 done
recv fd:5, len:100, 6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666
epoll wait timeout
[root@aaabbb]#
[root@aaabbb]#
客户端:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define LISTEN_BACKLOG (5)
#define BUF_SIZE (100)
#define REQUEST_STR "tcp pack"
void usage(void) {
printf("*********************************\n");
printf("./client 对端ip 对端端口\n");
printf("*********************************\n");
}
int main(int argc, char *argv[])
{
struct sockaddr_in client; // 客户端地址
struct sockaddr_in server; // 服务器地址
int sock_fd = 0;
int ret = 0;
socklen_t addrlen = 0;
char send_buf[BUF_SIZE] = {0};
char recv_buf[BUF_SIZE] = {0};
if (argc != 3) {
usage();
return -1;
}
char *ip = argv[1];
unsigned short port = atoi(argv[2]);
printf("ip:port->%s:%u\n", argv[1], port);
// 创建客户端套接字
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket error");
return -1;
}
printf("sock_fd: %d\n", sock_fd);
memset(&server, 0, sizeof(struct sockaddr_in));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(ip);
server.sin_port = htons(port);
ret = connect(sock_fd, (struct sockaddr *)&server, sizeof(struct sockaddr));
if (ret == -1) {
close(sock_fd);
perror("connect error");
return -1;
}
char seq = 0x31;
while(1) {
memset(send_buf, seq, BUF_SIZE);
send(sock_fd, send_buf, BUF_SIZE, 0);
printf("send %s\n", send_buf);
sleep(2);
seq++;
}
close(sock_fd);
return 0;
}
[root@aaabbb]#
[root@aaabbb]# ./socket-epoll-client 127.0.0.1 5000
ip:port->127.0.0.1:5000
sock_fd: 3
send 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
send 2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222
send 3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
send 4444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444
send 5555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555555
send 6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666
send 7777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777
[root@aaabbb]#
[root@aaabbb]#
6.8、套接字
套接字(Socket)是计算机网络通信中的一个基本概念,它是一种抽象的数据结构,用于在网络应用程序之间提供通信接口。套接字可以看作是一个端点,用于发送和接收数据,使得运行在不同机器上的应用程序能够交换信息,从而实现网络功能。
Linux套接字(Socket)是计算机网络中用于实现进程间通信的一种机制,它提供了一种标准化的方式,使得应用程序能够通过网络连接进行相互之间的通信。
网络通信的本质:本质上就是一种进程间通信。
6.8.1、端口号
端口号(port)是传输层协议的内容。端口号是一个2字节16位的整数。端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
IP地址 + 端口号 能够标识网络上的某一台主机的某一个进程。
一个端口号只能被一个进程占用。
因为端口号是隶属于某台主机的,所以端口号可以在两台不同的主机当中重复,但是在同一台主机上进行网络通信的进程的端口号不能重复。此外,一个进程可以绑定多个端口号,但是一个端口号不能被多个进程同时绑定。
6.8.2、TCP
TCP协议叫做传输控制协议(Transmission Control Protocol),TCP协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP协议是面向连接的,如果两台主机之间想要进行数据传输,那么必须要先建立连接,当连接建立成功后才能进行数据传输。其次,TCP协议是保证可靠的协议,数据在传输过程中如果出现了丢包、乱序等情况,TCP协议都有对应的解决方法。
特点:
传输层协议
有连接
可靠传输
面向字节流
6.8.3、UDP
UDP协议叫做用户数据报协议(User Datagram Protocol),UDP协议是一种无需建立连接的、不可靠的、面向数据报的传输层通信协议。
使用UDP协议进行通信时无需建立连接,如果两台主机之间想要进行数据传输,那么直接将数据发送给对端主机就行了,但这也就意味着UDP协议是不可靠的,数据在传输过程中如果出现了丢包、乱序等情况,UDP协议本身是不知道的。
特点:
传输层协议
无连接
不可靠传输
面向数据报
6.8.4、网络字节序
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。
(1)大端存储:低字节存放在高地址,高字节存放在低地址。
(2)小端存储:低字节存放在低地址,高字节存放在高地址。
网络字节序与主机字节序之间的转换:
#include <arpa/inet.h>
主机字节序转换为网络字节序【大端】----转换4字节的IP地址
uint32_t htonl(uint32_t hostlong);
主机字节序转换为网络字节序----转换2字节的端口号
uint16_t htons(uint16_t hostshort);
网络字节序【大端】转换为主机字节序 -----转换4字节的IP地址
uint32_t ntohl(uint32_t netlong);
网络字节序【大端】转换为主机字节序 -----转换2字节的端口号
uint16_t ntohs(uint16_t netshort);
6.8.5、socket
(1)常见接口
Linux socket函数说明:
(1) 创建socket文件描述符(TCP/UDP, 客户端+服务器)
int socket(int domain, int type, int protocol);
参数说明:
domain IP地址类型; 常用:AF_INET, AF_INET6
type 套接字类型
SOCK_STREAM:它提供基于字节流的有序、可靠、双向连接可以支持带外数据传输机制。【流式套接字------用于TCP通信】
SOCK_DGRAM:支持数据报(固定最大值的无连接、不可靠消息长度)。【报文套接字----UDP通信】
SOCK_SEQPACKET
protocol 默认0
返回值说明:
成功 返回文件描述符socketid
失败 -1
(2) 绑定端口号 (TCP/UDP, 服务器)
int bind(int socketid, const struct sockaddr *address, socklen_t addrlen);
参数说明:
socketid 套接字
address 服务器ip套接字结构体地址
addrlen 结构体大小
返回值说明:
成功 0
失败 -1
结构体struct sockaddr说明:
struct sockaddr {
sa_family_t sa_family; // 地址族,如AF_INET表示IPv4,AF_INET6表示IPv6
char sa_data[14]; // 用于存储具体地址信息的可变长度数组,但直接操作它并不推荐
};
在实际编程中,通常不会直接使用struct sockaddr,而是使用它的特定类型,如struct sockaddr_in(用于IPv4地址)和 struct sockaddr_in6(用于IPv6地址)。
struct sockaddr_in {
sa_family_t sin_family; // 地址族,对于IPv4地址,它总是AF_INET
uint16_t sin_port; // 端口号,网络字节顺序
struct in_addr sin_addr; // IPv4地址
// 通常还有以下填充字节,但这里省略了
// char sin_zero[8];
};
// 其中,in_addr的定义是
struct in_addr {
uint32_t s_addr; // IPv4地址,网络字节顺序
};
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族,对于IPv6地址,它总是AF_INET6
uint16_t sin6_port; // 端口号,网络字节顺序
uint32_t sin6_flowinfo; // IPv6 流标签和流量类别
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // 范围ID(用于链路本地地址等)
};
// 其中,in6_addr的定义是
struct in6_addr {
uint8_t s6_addr[16]; // 128位IPv6地址,网络字节顺序
};
(3) 开始监听socket(TCP, 服务器)
int listen(int socketid, int backlog);
参数说明:
socketid 套接字
backlog 已完成连接队列和未完成连接队列数之和的最大值128。一般这个参数为5。
返回值说明:
成功 0
失败 -1
(4) 接收请求(TCP, 服务器)
int accept(int socketid, struct sockaddr* address, socklen_t* address_len);
参数说明:
socketid 套接字
address 传入类型参数,获取客户端的IP和端口信息
address_len 结构体大小的地址
返回值说明:
成功 新的已连接套接字的文件描述符socketid_new
失败 -1
(5) 建立连接(TCP, 客户端)
int connect(int socketid, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
socketid 套接字【文件描述符】
addr 服务器套接字结构体地址(ip地址、端口号)
addrlen 结构体的长度
返回值说明:
成功 0
失败 -1
Linux中的read和write函数说明:
(1) 读取消息
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数说明:
fd 文件描述符(在socket编程中,它是socket描述符)。
buf 指向缓冲区的指针,该缓冲区用于存储从文件或socket读取的数据。
count 请求读取的字节数。
返回值说明:
成功时,返回读取的字节数(可能小于请求的字节数,特别是在非阻塞模式下或当达到文件末尾时)。
如果到达文件末尾,则返回0。
失败时,返回-1,并设置errno以指示错误(如EAGAIN、EINTR、EBADF等)。
(2) 写入消息
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数说明:
fd 文件描述符(在socket编程中,它是socket描述符)。
buf 指向包含要写入数据的缓冲区的指针。
count 要写入的字节数。
返回值说明:
成功时,返回写入的字节数(可能小于请求的字节数,尤其是在非阻塞模式下)。
失败时,返回-1,并设置errno以指示错误(如EAGAIN、EINTR、EBADF、EPIPE等)。
(3) 发送消息
send()函数用于在已连接的套接字上发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
sockfd 要发送数据的套接字描述符。
buf 指向包含要发送数据的缓冲区的指针。
len 要发送数据的字节数。
flags 控制选项,通常设置为0。
返回值说明:
成功 返回实际发送的字节数。
链接关闭 0
失败 -1
(4) 接受消息
recv()函数用于从已连接的套接字接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
sockfd 要接收数据的套接字描述符。
buf 指向用于存储接收数据的缓冲区的指针。
len 缓冲区的大小(以字节为单位。
flags 控制选项,通常设置为0。
返回值说明:
成功 返回实际接收到的字节数。
链接关闭 0
失败 -1
6.8.6、linux socket进程间通信
服务器:
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
std::cerr << "Socket creation failed" << std::endl;
return -1;
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(5000);
// 绑定套接字到指定端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
std::cerr << "Bind failed" << std::endl;
return -1;
}
// 开始监听
if (listen(server_fd, 5) < 0) {
std::cerr << "Listen failed" << std::endl;
return -1;
}
// 接受连接
while (true) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) >= 0) {
//std::cerr << "Accept failed" << std::endl;
break;
}
}
char buffer[1024] = {0};
int valread = read(new_socket, buffer, 1024);
std::cout << "Received from client: " << buffer << std::endl;
// 发送响应
const char *response = "Hello from server";
send(new_socket, response, strlen(response), 0);
close(new_socket);
close(server_fd);
return 0;
}
客户端:
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
std::cerr << "Socket creation error" << std::endl;
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(5000);
// 将服务器地址从字符串转换为网络地址
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
std::cerr << "Invalid address/ Address not supported" << std::endl;
return -1;
}
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
std::cerr << "Connection Failed" << std::endl;
return -1;
}
// 发送数据
const char *message = "Hello from client";
send(sock, message, strlen(message), 0);
char buffer[1024] = {0};
int valread = read(sock, buffer, 1024);
std::cout << "Received from server: " << buffer << std::endl;
close(sock);
return 0;
}
运行结果:
[root@aaabbb]# g++ socket-server.cc -o socket-server
[root@aaabbb]# ./socket-server
Received from client: Hello from client
[root@aaabbb]#
[root@aaabbb]#
[root@aaabbb]# g++ socket-client.cc -o socket-client
[root@aaabbb]# ./socket-client
Received from server: Hello from server
[root@aaabbb]#
[root@aaabbb]#
6.8.7、linux socket不同主机间通信
服务器端:
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
// 创建套接字
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
std::cerr << "Socket creation failed" << std::endl;
return -1;
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(5000);
// 绑定套接字到指定端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
std::cerr << "Bind failed" << std::endl;
return -1;
}
// 开始监听
if (listen(server_fd, 5) < 0) {
std::cerr << "Listen failed" << std::endl;
return -1;
}
// 接受连接
while (true) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t *)&addrlen)) >= 0) {
//std::cerr << "Accept failed" << std::endl;
break;
}
}
char buffer[1024] = {0};
int valread = read(new_socket, buffer, 1024);
std::cout << "Received from client: " << buffer << std::endl;
// 发送响应
const char *response = "Hello from server";
send(new_socket, response, strlen(response), 0);
close(new_socket);
close(server_fd);
return 0;
}
客户端:
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
std::cerr << "Socket creation error" << std::endl;
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(5000);
// 将服务器地址从字符串转换为网络地址
if (inet_pton(AF_INET, "服务器的IP地址", &serv_addr.sin_addr) <= 0) {
std::cerr << "Invalid address/ Address not supported" << std::endl;
return -1;
}
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
std::cerr << "Connection Failed" << std::endl;
return -1;
}
// 发送数据
const char *message = "Hello from client";
send(sock, message, strlen(message), 0);
char buffer[1024] = {0};
int valread = read(sock, buffer, 1024);
std::cout << "Received from server: " << buffer << std::endl;
close(sock);
return 0;
}
6.9、socket和epoll联合使用
问题:
1、问题:进程PID、端口号的区别:
进程ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念。
端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念。
一台机器上可能会有大量的进程,但并不是所有的进程都要进行网络通信,可能有很大一部分的进程是不需要进行网络通信的本地进程,此时PID虽然也可以标识这些网络进程的唯一性,但在该场景下就不太合适了。
Port和进程ID反映了一个进程使用的不同场景,在同一主机下使用进程ID标识一个进程,在网络通信中使用Port标识一个进程。
2、在 Linux 中,Socket 服务器和客户端上的套接字有以下一些不同之处:
角色和功能:
(1)服务器套接字主要用于监听来自客户端的连接请求,并在接受连接后与客户端进行通信。
(2)客户端套接字用于主动发起连接请求,以与服务器建立通信。
绑定和监听:
(1)服务器套接字通常需要绑定到一个特定的地址和端口,以便客户端能够找到它。然后,服务器会调用 listen
函数开始监听连接请求。
(2)客户端套接字一般不需要绑定,而是在发起连接时指定服务器的地址和端口。
连接建立:
(1)服务器通过 accept
函数接受客户端的连接请求,从而创建一个新的与客户端通信的套接字。
(2)客户端使用 connect
函数向服务器发起连接请求。
地址使用:
(1)服务器套接字通常使用一个众所周知的、固定的地址和端口,以便客户端能够预期并找到它。
(2)客户端套接字在连接时使用服务器的地址和端口。
并发处理:
(1)服务器可能需要同时处理多个客户端的连接,可能会采用多线程、多进程或异步 I/O 等方式来实现并发处理。
(2)客户端通常只与一个服务器进行通信,不需要处理多个并发连接。
生命周期:
(1)服务器套接字在服务器运行期间通常一直存在,持续监听连接。
(1)客户端套接字在与服务器完成通信或出现错误后可能会关闭。
总的来说,服务器套接字和客户端套接字在使用方式、功能和在通信过程中的角色上存在差异,以适应它们在网络通信中不同的职责。