IP地址:
IPV4—uint32_t类型的一个整数,用于在网络中唯一标识一台主机
IPV6—128位—没有推广起来—不向前兼容ipv4
但是由于网络使用的人口越来越多,因此ip不够用了,这时候大佬就想到了两种方式解决这个问题。
DHCP动态地址分配技术–谁上网给谁分配
NAT地址转换技术—大家使用同一个IP地址上网
端口:
unit_16_t 类型的一个整数–用于在一个主机上标识进程
无符号16位也就是2字节也就是16比特那么端口号的范围64k–0~65535
一个端口只能被一个进程占用,但是一个进程可以使用多个端口
协议:
通信协议:通信双方在通信过程中的数据格式约定
只有使用同一种标准网络通信协议才能实现网络互联
五元组:源IP地址、目的IP地址、源端口、目的端口、协议
协议分层:
按照每一层所提供的服务,以及所使用的接口,以及使用的协议,对网络通信环境进行分层,使网络通信更加简单灵活
1.以网络覆盖返回进行划分:局域网/城域网5~50km/广域网 因特网/互联网 :这也就是覆盖范围比较广的广域网
以太网/令牌环网
2.IP地址:在网络中唯一标识一台主机—ipv4: uint32_t ipv4实现所有人上网的解决方案:DHCP/NAT ipv6: uint8_t ip[16]
在网络传输中的每条数据中都必须包含源IP地址和目的IP地址
3.端口PORT: 在一台主机上唯一标识一个进程—unit16_t;
一个端口只能被一个进程占用,但是一个进程可以使用多个端口
在网络传输中的每条数据中都必须包含源端口和目的端口
4.协议:约定 通信协议:通信中数据格式的约定
协议分层:
OSI七层参考模型:应用层、表示层、会话层、传输层、网络层、链路层、物理层
TCP/IP五层模型
所有应用层协议都使用套接字层作为与传输层协议之间的接口。
应用层:负责应用程序之间的数据沟通; 应用层的协议都是程序员自己定义的–协议:HTTP/SSH/DNS/FTP
传输层:负责端与端之间的数据传输; 协议:TCP/UDP
网络层:负责地址管理与路由选择; 协议:IP; 设备:路由器
链路层:负责相邻设备之间的数据传输; 协议:以太网协议-Ethernet; 设备:交换机
物理层:负责光电信号的传输; 协议:以太网协议;设备:集线器
网络通信传输中数据的封装与应用:
封装:从上层到下层经过层层描述(结构体的封装)
分用:从下层到上层经过层层解析(下层解析,选择上层解析协议)
网络字节序:
字节序:CPU在内存中对数据存取的顺序
主机字节序:取决于CPU架构:
大端:低地址存高位
小端:低地址存低位
CPU架构-x86-小端 MIPS–大端
主机字节序对网络通信的影响:不同主机字节序的两台主机进行通信的时候有可能会造成数据二义
为了避免在网络传输中因为主机字节序出现数据二义 ,因此采用统一的大端字节序作为网络字节序
主机字节序会对哪些数据造成影响:字节序主要针对存储数据类型的大小大于一个字节的类型:short int long float double
char a[64]这是不需要的
通信中的两端主机:服务端和客户端
客户端:主动发起请求的一端
服务端:被动接收请求的一端
传输层协议的选择:
TCP协议:传输控制协议-面向连接,可靠传输,面向字节流
常用于对安全可靠性要求高与对性能要求的程序,例如:文件传输
UDP协议:用户数据报协议-无连接,不可靠,面向数据报
常用于对实时性要求高于安全性要求的程序,例如:视频传输
服务端:
1.创建套接字-通过套接字使进程与网卡建立联系
2.绑定地址信息-
主动端:描述数据的源端地址信息
被动端:描述哪些数据应该由当前进程处理
3.接收数据
4.发送数据
5.关闭套接字
客户端:
1.int socket(int domain(地址域),int type,int protpcol);
2.int bind(int sockfd,struct sockaddr* addr,socklen_t len)struct sockaddr_in
3.ssize_t sendto(int sockfd,char *buf,int len,int flag,struct sockaddr *destaddr,socklen_t len)
4.ssize_t recvfrom(int sockfd,char *buf,int len,int flag,struct sockaddr *peeraddr,socklen_t *len)
5.int close(int fd)
udp网络编程流程实现:
启动程序的时候服务端先运行因为udp并不保证可靠传输如果客户端直接运行会造成数据的流失
服务器:
注意事项:服务端必须主动绑定,因为需要保证地址永远不变才能被客户端找到
创建套接字socket
在内核中创建了一个socket结构体,通过这个结构体以及返回的文件描述符,是进程与网卡之间建立关联
为套接字绑定地址信息
在套接字结构体中标记地址信息,让操作系统知道发送数据的时候是通过哪些地址发送,以及接收到数据的时候,知道应该由哪一个进程来处理
数据的接收
数据的发送
关闭套接字,释放资源
数字字节序转换:主机字节序转换为网络字节序
uint32_t htonl(uint32_t hostlong)---4个字节的数据
uint16_t htons(uint16_t hostshort)---两个字节的数据
数字字节序转换:网络字节序转换为主机字节序
uint32_t ntohl(uint32_t netlong)---4个字节的数据
uint16_t ntohs(uint16_t netshort)---两个字节的数据
将点分十进制ip地址字符串转换为网络字节序ip地址:
in_addr_t inet_addr(const char *cp);
int inet_pton(int af,const char *src,void *dst);
将网络字节序ip地址字符串转换为字符串点分十进制ip地址:
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size)
char *inet_ntoa(struct in_addr in);
客户端:
注意事项:客户端不推荐为套接字绑定地址信息,而是在发送数据的时候由操作系统选择合适的地址信息进行绑定(因此客户端的地址有可能改变)---尽最大可能避免出现端口冲突的概率
int socket(int domain,int type,int protpcol);
domine:地址域
type:SOCK_STREAM---流式传输-可靠,有序,双向,基于连接的字节流传输
SOCK_DGREAM---无连接,不可靠,由最大长度限制的数据传输
protpcol:传输层所使用的协议--默认为0
IPPROTO_TCP/IPROTO_UDP
返回值:文件描述符--套接字操作句柄
int bind(int sockfd,struct sockaddr* addr,socklen_t len);
sockfd:创建套接字所返回的描述符
addr:地址结构
struct sockaddr_in{
unit_t_sin_family;
unit16_t sin_port;
struct in_addr sin_addr.s_addr;
}
ssize_t sendto(int sockfd,char *data,int len,int flags,struct sockaddr *destaddr,socklen_t addrlen)
socked:套接字描述符
data:发送的数据首地址
len:发送的数据长度
flag:选项参数 0-默认阻塞发送,MSG_DONTWAIT-非阻塞
destaddr:sockaddr_in 描述对端地址信息
addrlen:对端地址信息结构长度
返回值: >0-实际发送的数据长度、-1:错误
ssize_t recvfrom(int sockfd,char *buf,int len,int flags,struct sockaddr *peeraddr,socklen_t *addrlen)
socked:套接字描述符
buf:从socket接收缓冲区中取出数据放到buf中
len:要接收的数据长度
flag:0-默认阻塞接收(缓冲区中没有数据则一直等待)
peeraddr:源端地址信息,标识这条数据是谁发的
*addrlen:输入输出参数-用于指定想要获取的地址信息长度-以及返回实际的地址长度
返回值:>0-实际接收的数据长度,-1:错误
int close(int fd)
关闭套接字
hpp里面封装的接口
//实现以udpsocket类封装udp常用操作
#include
#include
#include
#include
#include
#include
#include
#include
#include
class UdpSocket{
public:
UdpSocket():_sock(-1){}
~UdpSocket(){};
bool Socket()
{
_sock = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
if(_sock < 0){
perror("socket error");
return false;
}
return true;
}
bool Bind(std::string &ip,uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(_sock,(struct sockaddr*)&addr,len);
if(ret < 0){
perror("bind error");
return false;
}
return true;
}
bool Recv(std::string &buf,struct sockaddr_in *saddr){
char tmp[1500] = {0};
socklen_t len = sizeof(struct sockaddr_in);
int ret = recvfrom(_sock,tmp,1500,0,(struct sockaddr*)saddr,&len);
if(ret < 0){
perror("recvfrom error");
return false;
}
//拷贝指定长度到buf里面
buf.assign(tmp,ret);
return true;
}
bool Send(std::string &buf,struct sockaddr_in *daddr){
socklen_t len = sizeof(struct sockaddr_in);
int ret = sendto(_sock,buf.c_str(),buf.size(),0,(struct sockaddr*)daddr,len);
if(ret < 0){
perror("sendto error");
return false;
}
return true;
}
bool Close(){
close(_sock);
_sock = -1;
}
private:
int _sock;
};
客户端
//通过udpSocket实现客户端程序
#include"udpsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc,char *argv[])
{
if(argc != 3){
printf("./udp_srv ip port\n");
return -1;
}
//这个地址是服务端的地址,为了让客户端知道数据请求发送到那里
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
UdpSocket sock;
CHECK_RET(sock.Socket());
//客户端并不推荐手动绑定地址
//CHECK_RET(sock.Bind(ip,port));
struct sockaddr_in srv_addr;
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(port);
srv_addr.sin_addr.s_addr = inet_addr(ip.c_str());
while(1){
std::string buf;
std::cout << "client say:";
fflush(stdout);
std::cin >> buf;
CHECK_RET(sock.Send(buf,&srv_addr));
CHECK_RET(sock.Recv(buf,&srv_addr));
std::cout << "server say:" << buf << std::endl;
}
sock.Close();
}
服务端
//通过udpSocket实现udp服务端程序
#include"udpsocket.hpp"
#define CHECK_RET(q) if((q) == false){return -1;}
int main(int argc,char *argv[])
{
if(argc != 3){
printf("./udp_srv ip port\n");
return -1;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
UdpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Bind(ip,port));
while(1){
std::string buf;
struct sockaddr_in cli_addr;
CHECK_RET(sock.Recv(buf,&cli_addr));
std::cout << "client say:" << buf << std::endl;
std::cout << "server say:";
fflush(stdout);
std::cin >> buf;
CHECK_RET(sock.Send(buf,&cli_addr));
}
sock.Close();
}
服务端:
1.创建套接字 socket
2.绑定地址信息 bind
3.开始监听 listen
告诉操作系统可以开始接收客户端的连接请求,进行三次握手建立连接
SYN--->ACK+SYN--->ACK
int listrn(int sockfd,int backlog)
backlog:限制同一时间的并发连接数
未完成连接队列/已完成连接队列--最大节点数量
4.从已完成连接队列中获取一个新的已完成连接(阻塞操作)
int accept(int sockfd,struct sockaddr* clitaddr,socklen_t *addrlen)
返回新连接的套接字描述符--以后与客户端通信的操作句柄
5.ssize_t send(int sockfd,char *data,int len,int flags)
6.ssize_t recv(int sockfd,char *buf,int len,int flags)
返回值:>0 实际接收的数据长度 ==0连接断开 <0出错
7.int close(int fd)
客户端:
1.创建套接字
2.为套接字绑定地址信息(不推荐主动绑定)
3.向服务端发起连接请求
int connect(int sockfd,struct sockaddr *srvaddr,socklen_t addrlen)
普通版本
#include"tcpsocket.hpp"
#include
int main(int argc,char *argv[])
{
if(argc != 3){
std::cout << "./tcp_srv ip port\n";
return -1;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(ip,port));
CHECK_RET(lst_sock.Listen());
while(1){
TcpSocket cli_sock;
std::string cli_ip;
uint16_t cli_port;
if(lst_sock.Accept(&cli_sock,&cli_ip,&cli_port) == false){
continue;
}
//cli_sock用于与指定的客户端进行通信
//lst_sock只用于获取新连接
std::cout << "new connect: "<< cli_ip << ":" << cli_port << "\n";
std::string buf;
bool ret = cli_sock.Recv(buf);
if(ret == false){
cli_sock.Close();
continue;
}
std::cout << "client say: " << buf << std::endl;
std::cout << "server say:";
fflush(stdout);
buf.clear();
std::cin >> buf;
ret = cli_sock.Send(buf);
if(ret == false){
cli_sock.Close();
continue;
}
}
lst_sock.Close();
return 0;
}
当前的服务端程序只有一个执行流,但是这个执行流却包含多种功能操作(accept/recvfrom/send),而每一种功能都有可能造成执行流阻塞
1.因为没有新连接的时候卡在accept这里,无法接受客户端的数据(无法与多个客户端进行通信)
2.卡在recv/send这里,无法获取下一个客户端的新连接,与其通信
当前的解决方案:使用多进程/多线程进行多任务处理,每个执行流只负责一个功能
主进程/线程只负责获取新连接,当新连接到来之后,直接创建子进程/普通线程进行与客户端通信,
这样的话就可以避免因为获取新连接而无法与客户端通信或者与客户端通信无法获取新连接
多进程版本注意事项:进程等待--使用信号回调,父进程不必等待子进程退出;
等待新连接的父进程,在每次获取新连接创建子进程之后,都需要将新套接字关闭。
多进程版本
#include
#include
#include
#include
#include"tcpsocket.hpp"
void sigcb(int signo){
while(waitpid(-1,NULL,WNOHANG)>0);
}
int main(int argc,char *argv[]){
if(argc != 3){
std::cout << "./tcp_process ip port\n";
return -1;
}
signal(SIGCHLD,sigcb);
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(ip,port));
CHECK_RET(lst_sock.Listen());
while(1){
TcpSocket cli_sock;
bool ret;
ret = lst_sock.Accept(&cli_sock);
if(ret == false){
continue;
}
std::cout << "new connect\n";
//创建子进程与客户端进行通信,父进程永远只管一件事,获取新连接
pid_t pid = fork();
if(pid < 0){
cli_sock.Close();
continue;
}else if(pid == 0){
while(1){
std::string buf;
cli_sock.Recv(buf);
std::cout << "client say:"<<buf << "\n";
std::cout << "server say:";
fflush(stdout);
buf.clear();
std::cin >> buf;
cli_sock.Send(buf);
}
cli_sock.Close();
exit(0);
}
cli_sock.Close();
}
lst_sock.Close();
return 0;
}
多线程版本的注意事项:通过创建普通线程时候的 传参/全局变量 将新获取的套接字传入普通线程中
在普通线程中与客户端进行通信,但是主线程千万不能关闭套接字(线程间文件描述符表是共享,共用同一份,在一个线程中关闭,其它线程也就关闭了)
tcp中recv返回值:连接断开对于接受数据的一方的体现
返回值==0,表示的不仅仅是没读到数据,而更想表示的是连接断开
连接断开时,对于发送方send的体现:send继续发送数据会触发异常导致进程退出
多线程版本
#include"tcpsocket.hpp"
#include
void *thr_start(void *arg)
{
TcpSocket *cli_sock = (TcpSocket*)arg;
while(1){
std::string buf;
bool ret = cli_sock->Recv(buf);
if(ret == false){
cli_sock->Close();
continue;
}
std::cout << "client say: " << buf << std::endl;
std::cout << "server say:";
fflush(stdout);
buf.clear();
std::cin >> buf;
ret = cli_sock->Send(buf);
if(ret == false){
cli_sock->Close();
continue;
}
}
cli_sock->Close();
return NULL;
}
int main(int argc,char *argv[])
{
if(argc != 3){
std::cout << "./tcp_srv ip port\n";
return -1;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[2]);
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(ip,port));
CHECK_RET(lst_sock.Listen());
while(1){
TcpSocket *cli_sock = new TcpSocket();
//TcpSocket cli_sock;
std::string cli_ip;
uint16_t cli_port;
if(lst_sock.Accept(cli_sock,&cli_ip,&cli_port) == false){
continue;
}
std::cout << "new connect: "<< cli_ip << ":" << cli_port << "\n";
pthread_t tid;
pthread_create(&tid,NULL,thr_start,(void*)cli_sock);
pthread_detach(tid);
//cli_sock用于与指定的客户端进行通信
//lst_sock只用于获取新连接
}
lst_sock.Close();
return 0;
}