一.UDP套接字
UDP通信流程
客户端
1.创建套接字
2.为套接字绑定地址(客户端不推荐主动绑定)
3.发送数据
4.接收数据
5.关闭套接字
#include"udpsocket.hpp"
#include<sstream>
using namespace std;
int main(int argc,char* argv[])
{
if(argc!=3)
{
cerr<<"./udp_cli ip port"<<endl;
return -1;
}
uint16_t port;
string ip=argv[1];
stringstream tmp;
tmp<<argv[2];
tmp>>port;
UdpSocket sock;
CHECK_RET(sock.Socket());
//客户端不推荐用户主动绑定,因为一个端口只能被一个进程占用,一旦固定端口,这个客户端只能启动一份
while(1)
{
string buf;
cin>>buf;
//当socket还未绑定地址,这时操作系统在发送之前可以检测到,
//操作系统会为socket选择一个合适的地址和端口进行绑定
sock.Send(buf,ip,port);
buf.clear();
sock.Recv(buf,ip,port);
cout<<"server say:"<<buf<<endl;
}
sock.Close();
return 0;
}
服务端
1.创建套接字
2.为套接字绑定地址
3.接收数据
4.发送数据
5.关闭套接字
#include"udpsocket.hpp"
#include<sstream>
using namespace std;
int main(int argc,char *argv[])
//argc:运行参数个数
{
if(argc!=3)
{
cerr<<"./udp_srv 192.168.216.128 9000"<<endl;
return -1;
}
uint16_t port;
string ip=argv[1];
stringstream tmp;
tmp<<argv[2];
tmp>>port;
UdpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Bind(ip,port));
while(1)
{
string buf;
string peer_ip;
uint16_t peer_port;
sock.Recv(buf,peer_ip,peer_port);
cout<<"client-["<<peer_ip<<":"<<peer_port<<"]say:"<<buf<<endl;
buf.clear();
cin>>buf;
sock.Send(buf,peer_ip,peer_port);
}
sock.Close();
}
udpsocket.hpp
//封装udpsocket类,实例化对象,向外提供简单的socket接口
//1.创建套接字
//2.绑定地址信息
//3.发送数据
//4.接收数据
//5.关闭套接字
#include<iostream>
#include<string>
#include<sys/socket.h>
#include<sys/types.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#define CHECK_RET(q) if((q)==false){return -1;}
//检测返回值
using namespace std;
class UdpSocket
{
private:
int _sockfd;
public:
bool Socket()
{
_sockfd=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
//AF_INET:ipv4
//SOCK_DGRAM:数据报套接字
//IPPROTO_UDP:传输层协议
if(_sockfd<0)
{
cerr<<"socket error"<<endl;
return false;
}
return true;
}
bool Bind(const string& ip,const uint16_t port)
{
struct sockaddr_in addr;
//ipv4地址域使用sockaddr_in
addr.sin_family=AF_INET;
//ipv4地址域
addr.sin_port=htons(9000);
//host to net short
//将主机字节序的16位数据,转换为网络字节序数据返回
addr.sin_addr.s_addr=inet_addr(ip.c_str());
//将点分十进制字符串IP地址转换为网络字节序IP地址
int ret;
socklen_t len=sizeof(struct sockaddr_in);
ret=bind(_sockfd,(struct sockaddr*)&addr,len);
if(ret<0)
{
cerr<<"bind error"<<endl;
return false;
}
return true;
}
bool Send(const string& data,const string& peer_ip,const uint16_t peer_port)
//peer:对端
{
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(peer_port);
addr.sin_addr.s_addr=inet_addr(peer_ip.c_str());
socklen_t len=sizeof(struct sockaddr_in);
int ret=sendto(_sockfd,&data[0],data.size(),0,(struct sockaddr*)&addr,len);
if(ret<0)
{
cerr<<"sendto error"<<endl;
return false;
}
return true;
}
bool Recv(string& buf,string& peer_ip,uint16_t& peer_port)
//成功返回实际接收的数据长度,失败返回-1
{
struct sockaddr_in peer_addr;
socklen_t len=sizeof(struct sockaddr_in);
char tmp[4096]={0};
int ret=recvfrom(_sockfd,tmp,4096,0,(struct sockaddr*)&peer_addr,&len);
if(ret<0)
{
cerr<<"recv error"<<endl;
return false;
}
peer_ip=inet_ntoa(peer_addr.sin_addr);
//将网络字节序IP地址转换为点分十进制字符串IP地址
peer_port=ntohs(peer_addr.sin_port);
//将网络字节序的16位数据转换为主机字节序数据
buf.assign(tmp,ret);
//从tmp中拷贝ret长度的数据到buf中
return true;
}
void Close()
{
close(_sockfd);
}
};
二.TCP套接字
TCP通信流程
客户端
1.创建套接字
2.为套接字绑定地址(不推荐主动绑定)
3.向服务端发起连接
4.发送数据
5.接收数据
6.关闭套接字
/tcp客户端通信流程
//1.创建套接字
//2.为套接字绑定地址信息(不推荐童虎主动绑定)
//3.向服务端发起连接请求
//4.发送数据
//5.接收数据
//6.关闭套接字
#include"tcpsocket.hpp"
#include<iostream>
#include<signal.h>
using namespace std;
void sigcb(int signo)
{
printf("recv a signo SIGPIPE---connect shutdoen\n");
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
cerr<<"./tcp_cli ip port"<<endl;
return -1;
}
signal(SIGPIPE,sigcb);
TcpSocket sock;
CHECK_RET(sock.Socket());
CHECK_RET(sock.Connect(argv[1],argv[2]));
while(1)
{
string buf;
cout<<"client say:";
fflush(stdout);
cin>>buf;
sock.Send(buf);
buf.clear();
sock.Recv(buf);
cout<<"server say:"<<buf<<endl;
}
sock.Close();
return 0;
}
服务端
1.创建套接字
2.为套接字绑定地址
3.开始监听
4.获取已完成连接
5.通过获取的已完成连接socket接收数据
6.通过获取的已完成连接socket发送数据
7.关闭套接字
//tcp服务端通信流程
//1.创建套接字
//2.为套接字绑定地址信息
//3.开始监听
//4.获取已完成连接socket
//5.通过获取的新建socket与客户端进行通信-接收数据
//5.发送数据
//7.关闭套接字
#include "tcpsocket.hpp"
#include<stdio.h>
using namespace std;
int main(int argc,char* argv[])
{
if(argc!=3)
{
cerr<<"./tcp_srv ip port"<<endl;
return -1;
}
TcpSocket lst_sock;
CHECK_RET(lst_sock.Socket());
CHECK_RET(lst_sock.Bind(argv[1],argv[2]));
CHECK_RET(lst_sock.Listen());
while(1)
{
TcpSocket clisock;
bool ret=lst_sock.Accept(clisock);
if(ret==false)
{
continue;
}
string buf;
ret=clisock.Recv(buf);
if(ret==false)
{
clisock.Close();
continue;
}
cout<<"client say:"<<buf<<endl;
buf.clear();
cout<<"server say:";
fflush(stdout);
cin>>buf;
clisock.Send(buf);
}
lst_sock.Close();
return 0;
}
tcpsocket.hpp
//封装一个tcpsocket类,向外提供简单的套接字接口
//1.创建套接字
//2.为套接字绑定地址信息
//3.开始监听
//4.向服务器端发起连接请求
//5.服务端获取新建连接
//6.发送数据
//7.接收数据
//8.关闭套接字
#include<iostream>
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string>
#include<sstream>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
using namespace std;
#define CHECK_RET(q) if((q)==false){return -1;}
class TcpSocket
{
private:
int _sockfd;
public:
void SetFd(int fd)
{
_sockfd=fd;
}
int GetFd()
{
return _sockfd;
}
bool Socket()
{
_sockfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
if(_sockfd<0)
{
cerr<<"socket error"<<endl;
return false;
}
return true;
}
int str2int(const string& str)
{
int num;
stringstream tmp;
tmp<<str;
tmp>>num;
return num;
}
bool Bind(const string& ip,const string& port)
{
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(str2int(port));
addr.sin_addr.s_addr=inet_addr(ip.c_str());
socklen_t len=sizeof(struct sockaddr_in);
int ret=bind(_sockfd,(struct sockaddr*)&addr,len);
if(ret<0)
{
cerr<<"bind error"<<endl;
return false;
}
return true;
}
bool Listen(const int backlog=5)
//开始监听:通知操作系统,可以开始接收客户端的连接请求
//并且完成三次握手建立连接过程
//tcp的面向连接,有一个三次握手建立连接过程
//backlog:客户端最大并发连接数(同一时间最多接收多少个客户端新连接请求)
{
int ret=listen(_sockfd,backlog);
if(ret<0)
{
cerr<<"listen error"<<endl;
return false;
}
return true;
}
bool Connect(const string& srv_ip,const string& srv_port)
{
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(str2int(srv_port));
addr.sin_addr.s_addr=inet_addr(srv_ip.c_str());
socklen_t len=sizeof(struct sockaddr_in);
int ret=connect(_sockfd,(struct sockaddr*)&addr,len);
if(ret<0)
{
cerr<<"connect error"<<endl;
return false;
}
return true;
}
bool Accept(TcpSocket &clisock,string *ip=NULL,uint16_t *port=NULL)
//sockfd:监听套接字描述符
//addr:客户端地址信息
//addrlen:地址信息长度
//返回值:返回新建连接的socket描述符,用于与客户端进行数据通信
{
struct sockaddr_in cliaddr;
socklen_t len=sizeof(struct sockaddr_in);
int newfd=accept(_sockfd,(sockaddr*)&cliaddr,&len);
if(newfd<0)
{
cerr<<"accept error"<<endl;
return false;
}
clisock.SetFd(newfd);
if(ip!=NULL)
{
*ip=inet_ntoa(cliaddr.sin_addr);
}
if(port!=NULL)
{
*port=ntohs(cliaddr.sin_port);
}
return true;
}
bool Send(string& data)
//sockfd:套接字你描述符(服务端是新建连接的socket描述符)
//buf:要发送的数据
//len:要发送的数据长度
//flags:0-默认阻塞发送
//返回值:成功返回实际发送的数据长度,失败返回-1
{
int ret=send(_sockfd,&data[0],data.size(),0);
if(ret<0)
{
cerr<<"send error"<<endl;
return false;
}
return true;
}
bool Recv(string& buf)
//flags:0-默认阻塞接收
//MSG_PEEK:从缓冲区取数据,但是数据并不从缓冲区移除
//返回值:>0返回实际接受的数据长度;=0连接断开;<0错误
{
char tmp[4096];
int ret=recv(_sockfd,tmp,4096,0);
if(ret<0)
{
cerr<<"recv error"<<endl;
return false;
}
else if(ret==0)
{
cerr<<"connect shutdown"<<endl;
return false;
}
buf.assign(tmp,ret);
return true;
}
bool Close()
{
close(_sockfd);
}
};
三.TCP协议
面向连接,可靠传输,面向字节流
协议字段信息
16位源/目的端口:实现端与端之间的数据传输
32位序号/确认序号:保证tcp数据的有序交付(包序管理)
4位首部长度:tcp报头长度(不包含数据),以4字节位单位,tcp报头大小范围:20~60字节
6位标志位:URG/ACK/PSH/RST/SYN/FIN
16位窗口大小:用于实现窗口滑动机制
16位校验和:校验接收的数据与发送的数据是否一致
16位紧急指针:带外数据
40字节选项管理:用到的时候才会有,意味着tcp报头长度不固定
面向连接:tcp的连接管理,三次握手,四次挥手
可靠传输
1.连接管理
2.确认应答机制
3.超时重传机制
4.协议字段中的序号/确认序号
5.协议字段中的校验和
可靠传输牺牲了部分性能,但有些损失是可以避免的
1.滑动窗口机制
通信双方在通信时会通过协议字段中的窗口字段协商窗口大小,告诉对方一次可以发送的最大数据量
通信双方通常在三次握手阶段,还会协商一个数据MSS(最大数据段大小–数据报中数据的最大长度)
发送方在发送数据的时候会将窗口大小的数据分成多个大小不大于MSS大小的数据报进行发送
1)流量控制
通过协议字段中的窗口字段,通知发送方能够发送的最大数据量,通过这个来限制对方的发送速度,避免发送过快,导致接收缓冲区塞满,而引起的后续数据丢包重传
2)快速重传
当接收方接收第二条数据,但是没有接收到第一条,则认为第一条有可能丢失,则立即向对方发送第一条数据的重传请求,并且将这个重传请求连续发送三次
发送方连续三次接收到重传请求,则对这条数据进行重传
连续发送三次重传请求,是为了避免有可能因为网络阻塞而接收到延迟的数据
(1)停等协议 (2)退n步协议 (3)选择重传协议
3)拥塞控制
发送方维护一个拥塞窗口,控制一次发送的数据量,以慢启动快增长的形式控制传输的数据量,起到对网络进行探测的作用,可以避免因为网络状况不好而导致的大量丢包
2.捎带应答机制
接收方为每一条接收到的数据组织报文,通过报文头部中的确认序号字段进行确认回复,这时如果刚好有要给对方发送的数据,则将这次的确认回复序号直接放到要发送的这条数据头中,可以节省一条空报文的回复,提高传输效率
3.延迟应答机制
接收方接收到数据之后,如果立即进行回复,窗口大小就会降低,导致传输吞吐率降低,降低了发送速度
这时如果接收数据之后,延迟一会进行确认回复,则又可能用户将缓冲区中的数据取走,保证传输吞吐率
提高性能
滑动窗口机制(流量控制,快速重传,拥塞控制)
延迟应答机制
捎带应答机制
字节流服务
send发送的数据,会先放到socket的发送缓冲区中,然后操作系统选择合适的实际将数据发送出去
多条小数据融合成一个大包发送出去,可以提高一定的传输性能
并且接收方,传输层数据的交付也比较灵活,可以一点一点交付,也可以一次性交付所有数据
但是字节流服务会造成数据的粘包问题:多条数据在缓冲区中粘连在一起
粘包的本质原因:tcp在传输层对数据的格式边界不敏感,不会替用户区分哪条数据从哪开始,到哪结束,只关注需要向用户交付多长字节的数据
因此粘包问题的解决方案就是用户在应用层进行数据的边界管理
1.特殊字符 2.数据定长 3.不定长数据在应用头中声明数据长度
tcp连接管理中保活机制
tcp通信双方若长时间无数据往来,则每隔一段时间向对方发送保活探测数据报,要求对方进行回复,若连续多条保活请求没有响应,则认为连接断开
连接断开在程序中的体现:recv读完所有数据之后,返回0,send触发异常SIGPIPE
传输层基于tcp实现的应用层协议:HTTP/FTP
四.UDP协议
无连接,不可靠,面向数据报
字段信息:16位源端口,16位目的端口,16位数据报长度,16位校验和
16位源/目的端口:实现端与端之间的数据传输
16位校验和:校验接收的数据与发送的数据是否一致(二进制反码求和算法)
16位数据报长度:udp数据报的总长度(包含udp头部信息在内)
无连接,不可靠:双方通信,不需要建立连接,只需要直到对方的地址信息就能发送数据
面向数据报:传输层向应用层交付数据的时候只能一整条一整条的交付
16位数据报长度,标识了一个完整的udp数据有多长,接收的收为了避免接收半条数据,导致缓冲区中的数据长度无法标识,导致交付混乱,因此,udp数据只能整条交付
决定了udp数据报最大长度不能超过64k
若是sendto给与的数据大于64k-8,则会发送失败
当发送的数据大于64k-8时,需要用户在应用层进行分包操作,分出的包大小不能大于64k-8
但是udp无法保证数据有序到达,因此当在应用层进行分包之后,还需要用户在应用层进行包序管理
传输层基于udp实现的应用层协议:DHCP,DNS
五.netstat工具
Netstat是控制台命令,是一个监控TCP/IP网络的非常有用的工具,它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。Netstat用于显示与IP、TCP、UDP和ICMP协议相关的统计数据,一般用于检验本机各端口的网络连接情况。
该命令的一般格式为 :netstat [-a][-e][-n][-o][-p Protocol][-r][-s][Interval]
命令中各选项的含义如下:
-a 显示所有socket,包括正在监听的。
-c 每隔1秒就重新显示一遍,直到用户中断它。
-i 显示所有网络接口的信息,格式“netstat -i”。
-n 以网络IP地址代替名称,显示出网络连接情形。
-r显示核心路由表,格式同“route -e”。
-t 显示TCP协议的连接情况
-u 显示UDP协议的连接情况。
-v 显示正在进行的工作。
-p 显示建立相关连接的程序名和PID。
-b 显示在创建每个连接或侦听端口时涉及的可执行程序。
-e 显示以太网统计。此选项可以与 -s 选项结合使用。
-f 显示外部地址的完全限定域名(FQDN)。
-o显示与与网络计时器相关的信息。-s 显示每个协议的统计。
-x 显示 NetworkDirect 连接、侦听器和共享端点。
-y 显示所有连接的 TCP 连接模板。无法与其他选项结合使用。
interval 重新显示选定的统计,各个显示间暂停的 间隔秒数。按 CTRL+C 停止重新显示统计。如果省略,则 netstat 将打印当前的配置信息一次。
常用选项n
etstat -a
本选项显示一个所有的有效连接信息列表,包括已建立的连接(ESTABLISHED),也包括监听连接请求(LISTENING)的那些连接。
netstat -b
该参数可显示在创建网络连接和侦听端口时所涉及的可执行程序。
netstat -s
本选项能够按照各个协议分别显示其统计数据。如果你的应用程序(如Web浏览器)运行速度比较慢,或者不能显示Web页之类的数据,那么你就可以用本选项来查看一下所显示的信息。你需要仔细查看统计数据的各行,找到出错的关键字,进而确定问题所在。
netstat -e
本选项用于显示关于以太网的统计数据,它列出的项目包括传送数据报的总字节数、错误数、删除数,包括发送和接收量(如发送和接收的字节数、数据包数 [1] ),或有广播的数量。可以用来统计一些基本的网络流量。
netstat -r
本选项可以显示关于路由表的信息,类似于后面所讲使用routeprint命令时看到的信息。除了显示有效路由外,还显示当前有效的连接。
netstat -n
显示所有已建立的有效连接。
netstat -p
显示协议名查看某协议使用情况
常见状态
即连接状态。在原模式中没有状态,在用户数据报协议中也经常没有状态,于是状态列可以空出来。若有状态,通常取值为:
LISTEN
侦听来自远方的TCP端口的连接请求
SYN-SENT
在发送连接请求后等待匹配的连接请求
SYN-RECEIVED
在收到和发送一个连接请求后等待对方对连接请求的确认
ESTABLISHED
代表一个打开的连接
FIN-WAIT-1
等待远程TCP连接中断请求,或先前的连接中断请求的确认
FIN-WAIT-2
从远程TCP等待连接中断请求
CLOSE-WAIT
等待从本地用户发来的连接中断请求
CLOSING
等待远程TCP对连接中断的确认
LAST-ACK
等待原来的发向远程TCP的连接中断请求的确认
TIME-WAIT
等待足够的时间以确保远程TCP接收到连接中断请求的确认
CLOSED
没有任何连接状态