客户端和服务器
网络通信是指两台计算机中的程序进行传输的过程
其中:
客户程序是指:主动发起通信的程序
服务程序是指:被动的等待,然后为向他发起通信的客户端提供服务的程序
比如最常见的上网,客户端是电脑或者手机上的浏览器软件,服务端是web软件
qq,客户端是qq软件,服务端是qq的后台
客户端必须提前知道服务端的ip地址和通信端口,而服务端只需要使用客户端传过来的ip地址,不需要提前知道客户端的ip地址
下面是网络通信的流程
我们下面编写最简单的客户端程序和服务端程序,
我们首先连接我的树莓派:192.168.185.246
可以看到如下界面我的本机地址是192.168.185.229,尝试在本机上运行客户端程序,树莓派运行服务端程序,首先在本机编写客户端程序
第一步:创建客户端的socket
string ip="192.168.185.246";
string port="5005"
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
{
perror("socket");
return -1;
}
创建socket,相当于准备电话机
第二部:向服务器发起连接请求
struct hostent* h;//存放服务端ip的结构体
if((h=gethostbyname(ip)===0))//把字符串格式的ip存到服务端代表的结构体中
{
cout<<"装入失败,请重试"<<endl;
close(sockfd);
return -1;
}
struct sockaddr_in servaddr;//用于存放ip和端口的结构体
memset(&servaddr,0,sizeof(servaddr));//初始化存放ip地址的空间;
servaddr.sin_family=AF_INET;
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);//将h的ip地址写入到存放ip地址和端口的结构体中
servaddr.sin_port=htons(atoi(port));//指定通信端口
if(connect(sockfd,(struct sockaddr *)&servaddr ,sizeof(servaddr))!=0)//简历链接;
{
perror("connect");
close(sockfd);
return -1;
}
打电话,电话接通后,
第三步:与服务端进行通信,发送一个报文等待回复,然后发送另一个报文
char buffer[1024];//存放请求报文内容
for(int i=0;i<3;i++)//进行三次通信
{
int res;//发送的结果
memset(buffer ,"你好,我是先辈,v我50掘我%d次",i+1);//生成请求报文
if((res=send(sockfd,buffer,strlen(buffer),0)<=0)//发送信息
{
perror("send");
break;
}
cout<<"已经成功发送字符"<<buffer<<endl;
memset(buffer,0,sizeof(buffer));//将发送信息初始化为空,作为接受回应报文的载体
if((res=recv(sockefd,buffer,sizeof(buffer),0))<=0)
{
cout<<"接受报文失败,状态码为:"<<res<<endl;
break;
}
cout<<"接受客户端传递回来的信息:"<<buffer<<endl;
}
进行聊天
第四步:关闭通信
close(socket)
服务端代码
服务端一共有六个步骤
1.创建服务端的socket
int port =5005;
int listenfd =socket(AF_INET,SOCK_STREAM,0);//创建一个socket,记录socket号码
if(listenfd==-1)//创建失败
{
peroor("socket");
return -1;
}
2.把服务端用于通信的ip地址和端口绑定到socket上
struct sockaddr_in servaddr;//存放服务端ip地址和端口的结构体
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;//指定协议
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);//服务器任意网卡的ip都可以用于通信
servaddr.sin_port=htonl(atoi(port));
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))!=0)//将服务端的ip和端口
{
perror("bind");
close(listenfd);
return -1;
}
3.把socket设置为监听状态
if(listen(listenfd,5)!=0)//设置socket为监听状态
{
perror("listen");close(listenfd);return -1;
}
开始接受请求
4.接受客户端的请求连接,如果没有连上来就利用accept()进行阻塞等待
int clienttf=accept(listenfd,0,0);//阻塞等待有没有其他客户端对他进行请求
if(clientfd==-1)
{
perror("accept");
close(listenfd);
return -1;
}
cout<<"客户端已经连接"<<endl;
5.等到有客户端连接上来了,与客户端通信,接受客户端发来的报文后返回一个报文
char buffer [1024];
while(true)
{
int res;
memset(buffer,0,sizeof(buffer));
if((res=recv(clientfd,buffer,sizeof(buffer),0))<=0)//阻塞等待客户端发送报文
{
cout<<"接收报文失败,返回状态为:"<<res;
}
cout<<"接收到:"<<buffer<<endl;
strcpy(buffer,"我已接收到消息");//覆盖到之前的接受的消息,重新写入回传信息
if((res=send(clientfd,buffer,strlen(buffer),0))<=0)//回传信息
{
cout<<"回传报文失败,状态码为:"<<res;
}
}
6.关闭socket,放弃资源
close(listenfd);//关闭服务器端用来监听的socket
close(clientfd);//关闭连接的传来的socket
注意,其实发送消息(send())和接收消息(recv())也是文件操作的一种,不过都是从对应的socket流中处理信息(万物皆文件),所以send()可以替代为write()函数,recv()可以替代为read()文件
网络编程的细节(重要)
socket函数
1.协议
人与人沟通的方式有很多种:书信,通话,qq,微信等等,需要先确定一种网络通信双方的协议,约定都用什么,才能说好
2.创建socket
int socket(int domain,int type,int protocol)
其中:
1.domain :通信协议族
PF_INET :ipv4协议族 最常用,其他都不常用
PF_INET6 :ipv6协议族
PF_LOCAL:本地通信协议族
PF_PACKET:内核通信协议族
PF_IPX:ipx协议族
2.type :数据传输的类型
SOCK_STREAM :面向连接的socket,数据可靠,不会丢失,数据不会错乱顺序,双通道
SOCK_DGRAM:无连接socket,数据不可靠,传输效率非常高,单向连接
3.protocol:最终使用的协议(tcp与udp)
IPPROTO_TCP:使用tcp协议(socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))
IPPROTO_UDP:使用udp协议(socket(PF_INET,SOCK_DGRAM,IPPROTO_UDP))
本参数也可以填0
tcp和udp
tcp的与udp特点比较:
1.tcp是面向连接的,通过三次握手建立连接,通过四次挥手断开连接,而upd是无连接,发送数据不需要建立连接,这样好处是高效率传输,坏处是无法确保数据的发送成功
2.tcp是可靠的通信方式,通过超时重传,数据检验等方式确保数据无差错,不丢失,不重复,不乱序到达,而upd是以最大的速率进行传输,尽最大能力交付,不保证数据丢失,重复等问题
3.tcp把数据当成字节流,出现网络波动或拥塞时,连接可能出现响应延迟,而upd 没有拥塞控制,出现拥塞时不会降低发送效率;
4.tcp只支持点对点通信,但udp支持一对一,多对多,一对多,多对一的通信
5.tcp报文首部比较大,为20字节,而udp只有8字节
6.tcp是全双工的可靠信道,而upd 是不可靠信道
tcp保证自己可靠的方式
数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组,
到达确认:接收端接收到分片数据时,根据分片的序号向对端回复一个确认包
超时重发:发送方在发送分片后开始计时,若超时却没有收到对端的确认包,将会重发分片
滑动窗口:TCP 中采用滑动窗口来进行传输控制,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方不会再发送数据
失序处理:TCP的接收端会把接收到的数据重新排序
重复处理:如果传输的分片出现重复,TCP的接收端会丢弃重复的数据
数据校验:TCP通过数据的检验和来判断数据在传输过程中是否正确
tcp和udp的使用场景
1:tcp:
tcp实现了数据传输的各种控制,适用于对可靠性有要求的场景
2.udp:
可以容忍数据丢失的场景
主机字节序与网络字节序
大端序与小端序
如果存储的数据类型占用的内存空间超过一个内存所占空间(1字节),那么他需要把这个数据连续放在多个内存里,cpu吧数据放在内存里的方式有两种
1.大端序:低字节节存放在高位,高位字节存放在地位,
2.小端序:低位字节存放在低位,高位字节存放在高位
比如假设现在我想存储一个12345678(16进制,两个数一个字节),申请4个字节的大小,假设是0x01,0x02,0x03,0x04,按照大端序的存储,就应该是0x01(内存的低位)存放12(数据的前两位,也就是高位)小端刚好相反,如下图所示
intel cpu默认按照小端方式进行存储
之前提到过,其实socket也就是一套文件流,只不过是网络的文件流,本质也是吧数据写入文件
当两台通信的双方存储的方式不同,加入大端序电脑通过socket发送给一台小端序电脑34,而小端序电脑接收到的就是43,这确实是一个问题
为了解决这个问题,传输过程中统一了一套字节序,叫网络字节序
大小短存储会带来的问题
在网络编程中,数据首发会有自动转存机智,不需要手动转换
但是在connect之前需要向sockaddr_in这个结构体中存入数据时,才需要考虑字节序的问题
比如,小端存进去一个ip地址,存储方式是反的,可能被socket也认为是反的
网络字节序
为了解决不同字节序之间计算机传输数据的问题,网络字节序约定使用大端序作为网络字节序
对于小端计算机,当接受到网络字节socket传来的大段数据时,需要利用c语言的库函数进行网络字节序与主机字节序之间的转换
uint16_t htons(unit16_t hostshort)//把16位整数从主机字节序转化为网络字节序
uint32_t htons(unit32_t hostlong)
uint16_t ntohs(uint16_t netshrot)
uint32_t ntohs(uint16_t netlong)//把32为整数从网络字节序转化为主机字节序
ip地址和通信端口
在计算机中,ipv4地址用4个字节的整数存放,通信端口用2字节的整数存放
connect中需要用到的结构体刨析
在简历连接时connect(sockfd,(struct sockaddr *)&servaddr ,sizeof(servaddr))
可以看到,第一个参数就是指定套接字,第二个参数就是这个结构体,我们知道套接字中是有协议族,数据传输类型,最终协议
并不包含IP地址和端口号,所以这个结构体就是用来存放ip地址和端口号的
scokaddr:
最基本的结构体,存放协议组,端口和地址信息,connect和bind函数需要用到这个结构体
struct sockaddr{
unsigned short sa_family;//协议族,与socket函数的第一个函数相同,填入AF_INET;
unsigned char sa_data [14];//14字节的端口和地址
}
scokaddr_in结构体
sockaddr结构体操作很不方便,就定义了sockaddr_in结构体,他的大小与sockaddr相等,并且可以进行强转
struct sockaddr_in
{unsingned short sin_family;//协议族,与socket()函数的第一个参数相同,填AF_INET;
unsingned short sin_port;//2字节的端口号
unsigned in_addr sin_addr;//4字节的地址
unsigned char sin_zero[8];//未使用,为了保持与sockaddr一样长
}
struct in_addr
{
unsigned int s_addr;//4字节的地址
}
观察上面的结构体成员,看到端口号是用大端序存储的,需要用htons函数转化
观察ip地址,需要把字符串转化为大端序的ip地址
下面给出两个方法,用来将string转化为大端序的ip结构体
方案一:用于客户端的gethostbyname
根据域名,主机名,字符串ip获取大端序ip的结构体,用于网络通信中确认结构体
其中函数的返回值是一个honstent*
struct hostent *gethostbyname(const char *name);
struct hostent {
char *h_name; // 主机名。
char **h_aliases; // 主机所有别名构成的字符串数组,同一IP可绑定多个域名。
short h_addrtype; // 主机IP地址的类型,例如 IPV4(AF_INET) 还是 IPV6。
short h_length; // 主机IP地址长度,IPV4地址为4,IPV6地址则为16。
char **h_addr_list; // 主机的ip地址,以网络字节序存储。
}
#define h_addr h_addr_list[0] // for backward compatibility.
当访问大端序的网络地址,直接用指针进行访问即可
方案2,用于客户端通信的c语言库函数
c语言提供了几个库函数,用于字符串格式的ip和大端序ip的互相转化,用于网络通信程序中
typedef unsigned int in_addr_t; // 32位大端序的IP地址。
// 把字符串格式的IP转换成大端序的IP,转换后的IP赋给sockaddr_in.in_addr.s_addr。
in_addr_t inet_addr(const char *cp);
// 把字符串格式的IP转换成大端序的IP,转换后的IP将填充到sockaddr_in.in_addr成员。
int inet_aton(const char *cp, struct in_addr *inp);
// 把大端序IP转换成字符串格式的IP,用于在服务端程序中解析客户端的IP地址。
char *inet_ntoa(struct in_addr in);
对socket进行封装
封装服务端的socket
网络编程涉及到多个数据结构和函数,使用起来很不方便
因此可以把客户端程序用到的数据结构和函数封装成ctcpclient类
被封装的类
class ctcpserver // TCP通讯的服务端类。
{
private:
int m_listenfd; // 监听的socket,-1表示未初始化。
int m_clientfd; // 客户端连上来的socket,-1表示客户端未连接。
string m_clientip; // 客户端字符串格式的IP。
unsigned short m_port; // 服务端用于通讯的端口。
public:
ctcpserver():m_listenfd(-1),m_clientfd(-1) {}
// 初始化服务端用于监听的socket。
bool initserver(const unsigned short in_port)
{
// 第1步:创建服务端的socket。
if ( (m_listenfd=socket(AF_INET,SOCK_STREAM,0))==-1) return false;
m_port=in_port;
// 第2步:把服务端用于通信的IP和端口绑定到socket上。
struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET; // ①协议族,固定填AF_INET。
servaddr.sin_port=htons(m_port); // ②指定服务端的通信端口。
servaddr.sin_addr.s_addr=htonl(INADDR_ANY); // ③如果操作系统有多个IP,全部的IP都可以用于通讯。
// 绑定服务端的IP和端口(为socket分配IP和端口)。
if (bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
close(m_listenfd); m_listenfd=-1; return false;
}
// 第3步:把socket设置为可连接(监听)的状态。
if (listen(m_listenfd,5) == -1 )
{
close(m_listenfd); m_listenfd=-1; return false;
}
return true;
}
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
bool accept()
{
struct sockaddr_in caddr; // 客户端的地址信息。
socklen_t addrlen=sizeof(caddr); // struct sockaddr_in的大小。
if ((m_clientfd=::accept(m_listenfd,(struct sockaddr *)&caddr,&addrlen))==-1) return false;
m_clientip=inet_ntoa(caddr.sin_addr); // 把客户端的地址从大端序转换成字符串。
return true;
}
// 获取客户端的IP(字符串格式)。
const string & clientip() const
{
return m_clientip;
}
// 向对端发送报文,成功返回true,失败返回false。
bool send(const string &buffer)
{
if (m_clientfd==-1) return false;
if ( (::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}
// 接收对端的报文,成功返回true,失败返回false。
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。
bool recv(string &buffer,const size_t maxlen)
{
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作buffer的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置buffer的实际大小。
return true;
}
// 关闭监听的socket。
bool closelisten()
{
if (m_listenfd==-1) return false;
::close(m_listenfd);
m_listenfd=-1;
return true;
}
// 关闭客户端连上来的socket。
bool closeclient()
{
if (m_clientfd==-1) return false;
::close(m_clientfd);
m_clientfd=-1;
return true;
}
~ctcpserver() { closelisten(); closeclient(); }
};
调用的函数
int main(int argc,char *argv[])
{
if (argc!=2)
{
cout << "Using:./demo8 通讯端口\nExample:./demo8 5005\n\n"; // 端口大于1024,不与其它的重复。
cout << "注意:运行服务端程序的Linux系统的防火墙必须要开通5005端口。\n";
cout << " 如果是云服务器,还要开通云平台的访问策略。\n\n";
return -1;
}
ctcpserver tcpserver;
if (tcpserver.initserver(atoi(argv[1]))==false) // 初始化服务端用于监听的socket。
{
perror("initserver()"); return -1;
}
// 受理客户端的连接(从已连接的客户端中取出一个客户端),
// 如果没有已连接的客户端,accept()函数将阻塞等待。
if (tcpserver.accept()==false)
{
perror("accept()"); return -1;
}
cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";
string buffer;
while (true)
{
// 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待。
if (tcpserver.recv(buffer,1024)==false)
{
perror("recv()"); break;
}
cout << "接收:" << buffer << endl;
buffer="ok";
if (tcpserver.send(buffer)==false) // 向对端发送报文。
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
}
}
封装客户端的socket
网络编程涉及到多个数据结构和函数,使用起来很不方便
因此可以把客户端程序用到的数据结构和函数封装成ctcpclient类
被封装的类
class ctcpclient // TCP通讯的客户端类。
{
private:
int m_clientfd; // 客户端的socket,-1表示未连接或连接已断开;>=0表示有效的socket。
string m_ip; // 服务端的IP/域名。
unsigned short m_port; // 通讯端口。
public:
ctcpclient():m_clientfd(-1) {}
// 向服务端发起连接请求,成功返回true,失败返回false。
bool connect(const string &in_ip,const unsigned short in_port)
{
if (m_clientfd!=-1) return false; // 如果socket已连接,直接返回失败。
m_ip=in_ip; m_port=in_port; // 把服务端的IP和端口保存到成员变量中。
// 第1步:创建客户端的socket。
if ( (m_clientfd = socket(AF_INET,SOCK_STREAM,0))==-1) return false;
// 第2步:向服务器发起连接请求。
struct sockaddr_in servaddr; // 用于存放协议、端口和IP地址的结构体。
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family = AF_INET; // ①协议族,固定填AF_INET。
servaddr.sin_port = htons(m_port); // ②指定服务端的通信端口。
struct hostent* h; // 用于存放服务端IP地址(大端序)的结构体的指针。
if ((h=gethostbyname(m_ip.c_str()))==nullptr ) // 把域名/主机名/字符串格式的IP转换成结构体。
{
::close(m_clientfd); m_clientfd=-1; return false;
}
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length); // ③指定服务端的IP(大端序)。
// 向服务端发起连接清求。
if (::connect(m_clientfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
::close(m_clientfd); m_clientfd=-1; return false;
}
return true;
}
// 向服务端发送报文,成功返回true,失败返回false。
bool send(const string &buffer) // buffer不要用const char *
{
if (m_clientfd==-1) return false; // 如果socket的状态是未连接,直接返回失败。
if ((::send(m_clientfd,buffer.data(),buffer.size(),0))<=0) return false;
return true;
}
// 接收服务端的报文,成功返回true,失败返回false。
// buffer-存放接收到的报文的内容,maxlen-本次接收报文的最大长度。
bool recv(string &buffer,const size_t maxlen)
{ // 如果直接操作string对象的内存,必须保证:1)不能越界;2)操作后手动设置数据的大小。
buffer.clear(); // 清空容器。
buffer.resize(maxlen); // 设置容器的大小为maxlen。
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); // 直接操作buffer的内存。
if (readn<=0) { buffer.clear(); return false; }
buffer.resize(readn); // 重置buffer的实际大小。
return true;
}
// 断开与服务端的连接。
bool close()
{
if (m_clientfd==-1) return false; // 如果socket的状态是未连接,直接返回失败。
::close(m_clientfd);
m_clientfd=-1;
return true;
}
~ctcpclient(){ close(); }
};
测试这个类
int main(int argc,char *argv[])
{
if (argc!=3)
{
cout << "Using:./demo7 服务端的IP 服务端的端口\nExample:./demo7 192.168.101.138 5005\n\n";
return -1;
}
ctcpclient tcpclient;
if (tcpclient.connect(argv[1],atoi(argv[2]))==false) // 向服务端发起连接请求。
{
perror("connect()"); return -1;
}
// 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。
string buffer;
for (int ii=0;ii<10;ii++) // 循环3次,将与服务端进行三次通讯。
{
buffer="这是第"+to_string(ii+1)+"个超级女生,编号"+to_string(ii+1)+"。";
// 向服务端发送请求报文。
if (tcpclient.send(buffer)==false)
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
// 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。
if (tcpclient.recv(buffer,1024)==false)
{
perror("recv()"); break;
}
cout << "接收:" << buffer << endl;
sleep(1);
}
}
tcp协议的三次握手与四次挥手
tcp是面向连接的,可靠的协议,简历tcp连接需要进行三次对话(三次握手),拆除tcp连接需要四次挥手
三次握手
1.当服务端调用listen函数进入监听状态等待连接之后,客户端就可以调用connetc函数发起tcp连接请求,
connect函数会触发三次握手机制,,三次握手完成之后,客户端与服务端将建立一个双向的传输通道
方框内表示此时socket的状态
socket状态确认
1.socket被创建的时候是close状态
当服务端进行等待时,socket进入监听状态
2.当客户端发送数据,第一次进行握手等待时,进入SYN_SENT(半连接)状态
当服务端第一次接收到来自服务端的报文3.之后,socket状态由listen(监听状态)转化为SYN_RECV(半发送状态),之后会传递会一个报文
4.当客户端接受到服务器端传来的报文时,代表连接成功,这时客户端socket由SYN_SENT转化为ESTABLISHED(已连接)状态
三次握手的具体细节
1.第一次握手,客户端向服务器端发送报文,要求建立客户端向服务端的传输通道,此时(SYN=1,SEQ=x)
2.第二次握手,服务端向客户端发送一个报文,这个报文分为两部分,一是对客户端发出客户端向服务端建立传输通道这一事件的回应,二是要求建立服务端向客户端的传输通道(SYN=1,ACK=1,seq=y,ack=x+1)
3.第三次是客户端做出对服务端发起的服务端向客户端进行传输通道的回应
(ACK=1,seq=x+1,ack=y+1)
带参数的三次握手的细节
1.第一次握手:SYN=1|,seq=x
SYN=1 :发出(客户端->服务端)的同步请求
SEQ=x 表示发送方随机选择的一个序号,标识数据发送的起始位置
2.第二次握手:SYN=1,ACK=1|,seq=y,ack=x+1
SYN=1:发出(服务端->客户端)的同步请求
ACK=1;对上一次的(客户端->服务端的请求)表示同意
SEQ=y 表示客户端随机选择的随即序号,用于表示客户端发送位置的起始位置
ack=x+1 确认已经接受到序列号为x的数据,并且期望下一个接受的序列号是x+1
3,第三次握手:ACK=1|,seq=x+1,ack=y+1
ACK=1:表示客户端同意(服务端->客户端的请求)
seq=x+1,标识数据开始传输的下一个序号(要发送的)
ack=y+1;对服务端要求发送的y序号的肯定,并且要求下一次给我发送y+1
总结,ack代表确认,seq代表要发送数据的地址,SYN表示请求的建立
其中,ACK,SYN代表在建立连接中的数据
而ack,seq表示在后续的建立中的
注意由于发送数据(seq)只能在建立连接之后,所以seq无大写
由于SYN是用于建立连接的同步信号,所以SYN只有大写
下面是一个形象的比喻
第一次握手:客户端对服务端说:我可以给你发送数据吗
第二次握手:服务端对客户端说:可以,不过我也要给你发送数据
第三次握手:客户端对服务端说:当然可以
至此,双向连接建立完成
注意:1.bind函数中,只有root用户可以是用全部端口,普通用户只能是用1024端口以上的函数
2.对于listen函数,第二个参数为已连接队列(socket=ESTABLISHED但是没有accept的连接,只存在于服务端中,对应完成二次握手,并没有完成三次握手的这种情况下说明服务端很忙)的大小,在高并发场景下需要调大一些
四次挥手
断开一个tcp连接,客户端和服务端需要相互总共发出4个报文确认连接的断开,在socket编程中,这一过程由任意一方执行close函数触发
1.A段对B端发出报文,要求拆除A到B的传输通道(此时FIN=1,seq=u)
2.B端对A端发出报文,确认A提出的拆除A到B的传输通道(此时ACK=1,seq=v,ack=u+1)
此时A到B没有传输通道了,所以拆除B到A的要B主动发起,并且不知道b有没有要对a发出的数据(所以提出的请求由b发出通知)
3.B端对A发出报文,提出拆除B到A的传输通道(此时FIN=1,ACK=1,seq=w,ack=u+1)
4.A对B发出关于报文,确认B提出的拆除B到A的传输通道(此时ACK=1,seq=u+1,ack=w+1)
下面是一个形象的比喻
1.A对B:我不会对你发数据了,断开链接吧
2.B回复:收到,(此时A不能对B发消息,但是B人可以对A发数据)
3.B发完数据了,对A说:我也不会给你发数据了(这时B也不能对A发数据了)
4.A回复:收到
细节 :
主动断开的四次挥手后,这个socket转化为TIME_WAIT状态,该状态持续2分钟,超过这个时间的报文将被丢弃
由于这个等待时间的机制存在,可能会出现两个问题
{1.当客户端主动断开时并不会对socket造成危害
2.当服务器主动断开时,会有两个危害,1.socket没有立即释放,2,端口只能在2MSL之后宠用
}
实现文件传输功能
采用socket通讯,客户端把指定文件传输给服务端
tcp缓存
系统为每个socket创建了==发送缓冲区和接受缓,应用程序调用send()/write()函数发送数据的时候,内核把数据从应用程序拷贝socket的发送缓冲区中,应用程序调用recv/read函数接收数据的时候,内核把数据从socket的接受缓冲区拷贝进应用程序中
即:发送数据就是把数据放入发送缓冲区
接收数据就是从接受缓冲区中取数据
接受缓冲区没有数据时,recv函数会进入阻塞状态,等待数据接受
当自己的发送缓冲区和对方的接受缓冲区都满了,那么send函数就会进行阻塞,
当客户端发送数据后,这个数据会存储在服务端的接收缓冲区中,就算此时客户端关闭了socket连接,服务端也能接收到数据
tcp的流量控制于拥塞控制
滑动窗口
发送窗口是发送缓冲区内的一段范围
接受窗口是接收缓冲区的一段范围
无论是客户端,还是服务端,都有自己的发送窗口和接收窗口
发送窗口结构
p1:已经发送并且接受到ack的,这是可以删掉的
p2:已经发送出去了,但是未收到ack
p3:允许发但是没有发送出去的
p4:不允许发的(没有可发的就是空的)
发送窗口的数据范围就是p2和p3,在数据未收到ack之前,允许发送数据的最大数据量
(当发送窗口收缩到0之后就不允许发送数据(此时接收方处理不过来)),当数据每收到一次ack,窗口就会往又滑动一段距离,这是p2缩短,p3伸长,多出来的p3由p4填充
接受窗口
p1:已经被读并且发送ack的,(可以删除了)
p红=p黄+p蓝:由于收包并不是顺序的,所以黄色的是并未接受的,而蓝色已经收到了(应用程序每读取一次,窗口就滑动一次)
p4:不允许接受的
注意接受数据和读取数据是分离开的,接收数据法僧在红框,读取数据是在p1阶段,
假设
应用程序很慢,读取的很慢红框被撑满,此时就无法接受了
流量控制
当接收方接受速率很差时,导致接受速率小于发送速率,接收方就会丢包,丢包之后,发送发又会重传,重传的又被丢,如此恶化下去,浪费网络带宽
场景:发生在接受方,中间传输好,两头差(接受处理能力差)
控制方法:接收方可与动态调节自己接收窗口的大小,发送方随着接收方滑动窗口的调整而调整
注意 :当发送方窗口收缩到0的时候,不会发送数据,进行对接收方的ack的等待
但是这时候,接收方发送的ack丢掉了,还在等待重传
但是由于发送发窗口为0,无法进行重传,此时 就发生了死锁
解决方法:当发送窗口收缩到0之后,发送方启动一个定时器,定时给接受发发送探测报文,询问窗口能否扩大,如果可以扩大就扩大,多次询问还是无法发送之后就可能关闭连接了
提高网络吞吐率的基本思想
1.避免小包传输(小包也需要ack,为什么不直接用大包呢),所以发送方和接收方都会进行一个延迟的策略。把小包堆积成大包批量 发送
发送方延迟:累积一批一次性发送nagle算法
接收方延迟:累计ack发送
nagle算法
默认都是开启的,在高并发或者低延迟的场景下应该禁止(设置tcp的tcp_NODELAY)
算法核心原则:任意时刻,只允许有一个未被ack的小包存在
一共有四个条件
1.数据长度高
2.设置fin(关闭)
3,设置tcp_nodelay
4.已经发生超时
都满足时才会发送,不满足的就放在发送缓冲区中,暂时不发送
(都放在缓冲区里了,牺牲了延迟获得了吞吐率)
接收方的延迟ack
拥塞控制
当中间的传播途径(发生在网络层)处理不过来时,网络进行拥塞,接收方接收不到ack就会进行重传,从而导致网络更加拥塞
场景:发生在中间,两头好,中间差(网络能力差)