OSI七层模型
OSI(Open System Interconnection,开放系统互连)七层网络模型称为开放式系统互联参考模型,是一个逻辑上的定义和规范,可以通过下图看出两个主机通过这七层协议层层递进以实现网络传输。
它既复杂又不实用,所以一般我们使用TCP/IP五层(或四层)模型
TCP/IP五层模型
应用层
负责程序间沟通,网络编程主要涉及该层
Telnet/FTP/e-mail
传输层
负责两台主机之间的数据传输,能够确保数据可靠的从源主机发送到目标主机(数据在这里叫段)
TCP/UDP
网络层
地址管理和路由选择(数据在这里叫数据报)
路由器
数据链路层
负责数据之间数据帧的传送和识别(数据在这里叫帧)
以太网、无线LAN、交换机
物理层
物理层决定了最大传输速率、传输距离,负责光/电信号的传递方式
集线器、调制解调器
网络传输基本流程
应用层上的数据从逻辑上来说两台主机是在应用层进行通信的,但实际上在物理层上来说,发送方层层向下通过以太网传输给接收方,接收方层层向上才获得的数据,每一层在逻辑上都认为自己是和对方的同一层直接通信的。
每层协议都有自己的协议定制方案,每层协议都有自己的协议报头
报头封装情况如下所示:
数据传输时会层层向下到达物理层进行传输,局域网中可以直接传输,但在跨网段数据传输时,我们需要需要使用ip地址(4字节)寻找目的地址,并使用路由器进行传输。
在局域网传输数据时,局域网内所有的主机可以抽象成在一根线上,但两个主机通信时会使用每个主机特有的MAC地址进行通信,其他的同样在路径上的主机收到后发现MAC地址对不上就会把数据丢出去,直到传达给正确的MAC地址,局域网内会发送数据碰撞,也就是某一个主机发送消息时,接收放也在等待另一个主机的消息。
在跨网段传输数据时,最重要的是ip和mac两个概念。两个主机的ip地址相当于它们起始和目的地的ip地址,而mac地址会随着经过的中间路程而变化,直到最后发现是目的主机的mac地址,这时传输成功
1. 假设网络上要将一个数据包(名为PAC)由北京的一台主机(名称为A,IP地址为IP_A,MAC地址为MAC_A)发送到华盛顿的一台主机(名称为B,IP地址为IP_B,MAC地址为MAC_B)
2. A在将PAC发出之前,先发送一个ARP请求,找到其要到达IP_B所必须经历的第一个中间节点C1的MAC地址M1,然后在其数据包中封装(Encapsulation)这些地址:IP_A、IP_B,MAC_A和M1
3. 在传输过程中,IP_A、IP_B和MAC_A不变,而中间节点的MAC地址通过ARP在不断改变(M1,M2,M3),直至目的地址MAC_B
源ip地址和目标ip地址指的是最开始传送的主机地址和最后接收到的主机地址,一般在网络通信的过程中这两个都不会改变。
网路编程套接字
我们先来认识一下什么是套接字:
先从网络的原理说起:网络中服务端和客户端本质是在不同主机上的进程,网络通信其实就是进程间通信,而端口号是一个2字节16位的整数,用于唯一标识这个主机上的进程,告诉操作系统要交给哪个进程处理,当网络到达这个主机需要调用进程时,判断的就是它的端口号。
一个进程可以由很多个端口号标识,但是一个端口号只能标识一个进程
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程,这个就叫做套接字
套接字可以分为三类:原始socket、域间socket、网络socket(我们在网络编程中主要使用网络socket)
网络字节序
网络传输过程中,主机收到信息后会需要进行信息读取,如果两个主机的网络通信中,一个为大端存储,一个为小端存储,那么它们之间通信的数据,就会出现错误。
大小端区别如下:
所以我们规定,在网络传输中,只能使用大端来存储字节,如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可。
相关的地址转换函数(转为网络字节序):
htons/htonl 将本地端口转为网络端口
本地ip转为网络ip:
1.inet_addr
2.inet_pton和inet_ntop (可兼容ipv6)
3.inet_aton
相关编程接口
创建socket :
int socket(int domain, int type, int protocol)
绑定端口号bind:
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
监听listen:
int listen(int socket, int backlog);
接收请求accept:
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
建立连接connect(客户端):
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
使用netstat 指令可以得知整个 Linux 系统的网络情况:
netstat -u 显示udp的连线情况
netstat -t 显示tcp的连线情况
具体命令的使用可以参考: Linux netstat命令 | 菜鸟教程 (runoob.com)
建立一个简单的UDP和TCP服务器
UDP
UDP服务器主要代码(完整代码在最后)
class UdpServer
{
public:
UdpServer(uint16_t port,std::string ip = ""):_ip(ip),_port(port)
{}
bool initServer()
{
//1.创建套接字
_socket = socket(AF_INET,SOCK_DGRAM,0);
//创建失败
if(_socket < 0)
{
logMessage(FATAL,"%d-%s",errno,strerror(errno));
exit(-1);
}
//2.使用bind进行绑定
//int bind(int socket, const struct sockaddr *address,socklen_t address_len);
//所以我们需要先创建一个sockaddr,并初始化
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET; //家族地址
local.sin_port = htons(_port); //必须要转成网络形式
//sin_addr存储IP地址,使用in_addr这个数据结构
//s_addr按照网络字节顺序存储IP地址
//inet_addr() 函数的作用是将点分十进制的IPv4地址转换成网络字节序列的长整型
//INADDR_ANY 可以在服务器工作中,从任意ip获取数据
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
//开始bind
if(bind(_socket,(struct sockaddr*)&local,sizeof(local)) < 0)
{
//如果绑定失败
logMessage(FATAL,"%d-%s",errno,strerror(errno));
exit(2);
}
logMessage(NORMAL,"bind done...%s",strerror(errno));
return true;
}
void start()
{
//服务器要一直不退出
char buffer[SIZE];
while(1)
{
//用一个输出型参数接受传来的sockaddr
struct sockaddr_in rev;
bzero(&rev,sizeof rev);
//获取长度
socklen_t len = sizeof rev;
//3.接收消息
ssize_t s = recvfrom(_socket,buffer,sizeof(buffer),0,(struct sockaddr*)&rev,&len);
//实现
std::string echo_cmd;
char result[256];
char key[64];
if(s > 0)
{
//3.1 普通输出
buffer[s] = 0; //目前数据当作字符串
uint16_t f_port = ntohs(rev.sin_port); //网络地址转成主机地址
std::string f_ip = inet_ntoa(rev.sin_addr);
printf("[%s]:%d say->%s\n",f_ip.c_str(),f_port,buffer);
}
//3.1 还需要返还回去
sendto(_socket,buffer,sizeof(buffer),0,(struct sockaddr *)&rev,len);
}
}
~UdpServer(){
if(_socket > 0)
close(_socket);
}
private:
std::string _ip; //ip地址
uint16_t _port; //端口
int _socket; //套接字
std::unordered_map<std::string,struct sockaddr_in> _users;
};
UDP客户端主要代码(多线程版本)
uint16_t serverport = 0;
std::string serverip;
static void usage(std::string proc)
{
std::cout << "\nUsage:" << proc << " :ip port" << std::endl;
}
static void *udp_send(void *arg)
{
int sock = *(int *)(((ThreadData *)arg)->_args);
std::string name = ((ThreadData *)arg)->_name;
std::string messages;
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while (1)
{
std::cout << "请输入消息:";
std::getline(std::cin, messages);
if (messages == "quit")
break;
// 发送信息,要把自己的server信息发过去
sendto(sock, messages.c_str(), messages.size(), 0, (struct sockaddr *)&server, sizeof server);
}
}
static void *udp_rev(void *arg)
{
int sock = *(int *)(((ThreadData *)arg)->_args);
std::string name = ((ThreadData *)arg)->_name;
char buffer[1024];
// 接收消息,需要一个空的rev来接收
while (1)
{
struct sockaddr_in rev;
socklen_t len = sizeof rev;
int s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&rev, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << "echo->" << buffer << std::endl;
}
else if (s == 0)
{
logMessage(NORMAL, "can`t read message");
}
else
{
logMessage(ERROR, "read message failed");
break;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
// 获得套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
// 多线程
// 存储传进来的ip和port
serverip = argv[1];
serverport = atoi(argv[2]);
// 多线程的创建
// 使用智能指针,一个线程接收消息,一个线程发送消息
std::unique_ptr<Thread> sendT(new Thread(1, udp_send, (void *)&sock));
std::unique_ptr<Thread> revT(new Thread(2, udp_rev, (void *)&sock));
sendT->start();
revT->start();
sendT->join();
revT->join();
close(sock);
return 0;
}
TCP
TCP服务器主要代码(完整代码在最后)
static void service(int sock,int port,std::string ip)
{
char buffer[1024];
while(1)
{
//可以直接使用read和write
ssize_t s = read(sock,buffer,sizeof(buffer)-1);
if(s > 0)
{
buffer[s] = 0;
//printf("[%s]:[%d]-># %s",ip.c_str(),port,buffer);
std::cout << ip << ":" << port << "# " << buffer << std::endl;
}
else if(s == 0) //写端关闭连接
{
logMessage(NORMAL, "%s:%d shutdown, me too!", ip.c_str(), port);
break;
}
else
{
logMessage(ERROR,"read socket error, %d:%s", errno, strerror(errno));
break;
}
//写回数据
write(sock,buffer,sizeof buffer);
}
//关闭accpet返回的请求
close(sock);
}
class TcpServer
{
private:
const static int gbacklog = 20;
// static void* ThreadFunc(void* args)
// {
// //告诉主线程别等我了
// //线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源)
// pthread_detach(pthread_self());
// //编译器隐式执行的任何类型转换都可以由static_cast显式完成
// ThreadData *td = static_cast<ThreadData *>(args);
// service(td->_sock,td->_port,td->_ip);
// delete td;
// return nullptr;
// }
public:
TcpServer(uint16_t port,std::string ip = "")
:_port(port),_ip(ip),_listensock(-1),
_threadPtr(ThreadPool<Task>::getInstance())
{}
bool initServer()
{
//这是一个文件描述符
_listensock = socket(AF_INET,SOCK_STREAM,0);
//判断是否创建失败
if(_listensock < 0)
{
logMessage(FATAL,"crteate socket error,%d-%s",errno,strerror(errno));
exit(2);
}
//获取套接字成功
logMessage(NORMAL,"create socket success");
//准备进行绑定
struct sockaddr_in local;
memset(&local,0,sizeof local);
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = _ip.empty()? INADDR_ANY : inet_addr(_ip.c_str());
//新型网路地址转化函数inet_pton,/IPv6和IPv4都适用
//inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
//把ip地址转化为用于网络传输的二进制数值
//inet_aton(_ip.c_str(), &local.sin_addr);
if(bind(_listensock,(struct sockaddr*)&local,sizeof local) < 0)
{
logMessage(FATAL,"create bind failed,%d-%s",errno,strerror(errno));
exit(1);
}
//因为TCP是面向连接的,当我们正式通信的时候,需要先建立连接
//listen是告诉内核等待客户端的连接
if(listen(_listensock,gbacklog) < 0)
{
logMessage(FATAL,"create listen failed,%d-%s",errno,strerror(errno));
exit(3);
}
logMessage(NORMAL,"create listen success");
return true;
}
void Start()
{
_threadPtr->run();
while(1)
{
//输出型参数使用
struct sockaddr_in src;
socklen_t len = sizeof src;
//如果服务器发送请求连接,就需要用accept接收
int servicesock = accept(_listensock,(struct sockaddr*)&src,&len);
if(servicesock < 0)
{
logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
continue;
}
//获取连接成功了
uint16_t client_port = ntohs(src.sin_port);
std::string client_ip = inet_ntoa(src.sin_addr);
logMessage(NORMAL,"accept success [%s]:[%d]",client_ip.c_str(),client_port);
//进行通信
//version1 单进程版
//传入一个accept的返回值(连接序号),和连接过来的客户端的端口、ip
service(servicesock,client_port,client_ip);
}
}
~TcpServer(){}
private:
std::string _ip;
uint16_t _port;
int _listensock; //建立连接的套接字
//线程池版本
std::unique_ptr<ThreadPool<Task>> _threadPtr;
};
TCP客户端主要代码
int main(int args, char *argv[])
{
if (args != 3)
{
std::cout << "please input ip and port" << std::endl;
exit(1);
}
uint16_t port = atoi(argv[2]);
std::string ip = argv[1];
std::string message;
bool alive = false; // 判断是否连接着
int sock = 0;
while (1)
{
if (!alive)
{
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cout << "create socket failed" << std::endl;
exit(2);
}
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip.c_str());
// 申请连接
if (connect(sock, (struct sockaddr *)&server, sizeof server) < 0)
{
std::cerr << "connect error" << std::endl;
exit(3);
}
std::cout << "connect success" << std::endl;
alive = true; // 连接成功了,所以活过来了
}
std::cout << "请输入信息# ";
std::getline(std::cin, message);
if (message == "quit")
break;
// send和recv和read、write有点像,就是多了个是否阻塞(?
ssize_t s = send(sock, message.c_str(), message.size(), 0);
if (s > 0)
{
char buffer[1024];
ssize_t s = recv(sock, buffer, sizeof buffer, 0);
if (s > 0)
{
buffer[s] = 0;
std::cout << "回显# " << buffer << std::endl;
}
else if (s == 0)
{
alive = false;
// 关闭连接
close(sock);
}
}
else
{
alive = false;
close(sock);
}
}
}