【网络篇】socket编程——TCP(史上最全)

春风从不入睡、 2024-07-12 15:37:01 阅读 65

目录

一、初始TCP

1.TCP协议特点

2.TCP头:

 3.确认应答机制

4.超时重传机制

5.流量控制

6.拥塞控制

(1)TCP 的拥塞控制方法

慢开始

拥塞避免

快重传

快恢复

二、建立连接——三次握手

 三、断开连接——四次挥手

四、socket编程

##客户端API函数

##服务端API函数


一、初始TCP

1.TCP协议特点

(1)TCP 是<code>面向连接的运输层协议。应用程序在使用 TCP 协议之前,必须先建立 TCP 连接。在传送数据完毕后,必须释放已经建立的 TCP 连接

(2)每一条 TCP 连接只能有两个端点,每一条 TCP 连接只能是点对点的(一对一)

(3)TCP 提供可靠交付(确认机制、拥塞控制、流量控制、超时重传的服务。通过 TCP 连接传送的数据,无差错、不丢失、不重复,并且按序到达

(4)TCP 提供全双工通信。TCP 允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接受缓存,用来临时存放双向通信的数据

(5)面向字节流。TCP 中的“流”指的是流入到进程或从进程流出的字节序列

2.TCP头:

 3.确认应答机制

确认应答机制是TCP可靠性中核心之一

接收方回复收到的一个应答报文(ACK),表示已经收到

例如:你叫朋友一起出去玩,TCP中的确认应答机制如下所示。针对发送的请求进行编号,应答时也针对相应的编号应答,这样就能保证数据传输的可靠性。

4.超时重传机制

超时重传也是TCP可靠性保证的必要条件之一

确认应答是比较理想的情况,但数据在传输过程中,可能是会丢包的

我们以上面叫朋友去玩举例:A 给 B 发消息,你在家嘛?等了很久,A 也没收到 B 的消息,此时,存在以下几种情况:

(1) B 不想回 A 的消息

TCP 抱着一种 “悲观的态度”,当一次丢包重传之后,TCP 就觉得大概率后面的重传也没用,所以就隔一个更长的时间,节省带宽

(2)B 没收到 A 的消息 (丢包情况: 发的请求丢失)

(3)B 回复了消息,但 A 没收到 (丢包情况:应答的 ACK 丢失,重传就意味着接收到相同数据)

(2)(3)情况:丢包的两种情况,对于发送方来说无法确定是哪种情况,因此,进行统一处理:当发送了一条数据之后,TCP 内部就会自动启动一个定时器,达到一定时间也没收到 ACK,定时器就会自动触发重传消息的动作 —— 超时重传

5.流量控制

流量控制(flow control):让发送方的发送速率不要太快,要让接收方来得及接收

利用滑动窗口机制可以很方便地在 TCP 连接上实现对发送方的流量控制,实质就是TCP在发送数据的时候将数据放到发送缓冲区,将接收的数据放到接收缓冲区。而流量控制要做的事情就是通过接收缓冲区的大小,控制发送端的发送,如果对方的接收缓冲区满了,就不能继续发送。为了控制发送端的速率,接收端在进行ACK确认时会携带自身的窗口(rwnd)大小,就会告知发送端自己缓冲区的大小,进行相应的流量控制。

6.拥塞控制

<code>拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制所要做的都是一个前提,就是网络能够承受现有的网络负荷

(1)TCP 的拥塞控制方法

TCP 进行拥塞控制的算法有四种,即慢开始(slow-start)、拥塞避免(congestion avoidance)、快重传(fast retransmit)和快恢复(fast recovery)

慢开始

当主机开始发送数据时,由于并不清楚网络的负荷情况,如果立即把大量数据字节注入到网络,就有可能引起网络发生拥塞。经验证明,较好的方法是先探测一下,即由小到大逐渐增大发送窗口,也就是说,由小到大逐渐增大拥塞窗口数值。

在执行慢开始算法时,发送方每收到一个队新报文段的确认 ACK,就把拥塞窗口值加1,然后开始下一轮的传输。因此拥塞窗口 cwnd 随着传输轮次按指数规律增长。当拥塞窗口 cwnd 增长到慢开始门限值 ssthresh 时,就改成执行拥塞避免算法,拥塞窗口按线性规律增长

ssthresh:慢开始门限,一般会有一个初始值

拥塞避免

让拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加1,而不是像慢开始阶段那样加倍增加。拥塞窗口 cwnd 按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多

“拥塞避免”并非完全能够避免拥塞,而是把拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞

快重传

采用快重传算法可以让发送方尽早知道发生了个别报文段的丢失。快重传算法首先要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认,即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认

快恢复

发送方知道当前只是丢失了个别的报文段。于是不启动慢开始,而是执行快恢复算法。这时,发送方调整门限值 ssthresh = cwnd / 2 ,同时设置拥塞窗口 cwnd = ssthresh ,并开始执行拥塞避免算法

二、建立连接——三次握手

建立TCP连接共有三步,即三次握手

先简单的给大家举个例子,我们可以把他类比于平时打电话,如下:

 

第一次A不知道B能否听到自己的声音,A对B说“喂,你能听到吗?”,第二次B听到后还需要知道A是否也能听到自己的声音,就是B对A的回话“我能听到,你呢”,第三次A的回复确认双方都能听到声音,也就是A对B的回复“我也能”,由上三次就能保证通话的正常,类似于网络建立连接时的三次握手。

两次握手可以吗??

不可以

两次握手只能保证单向连接是通畅的,TCP 协议是双向的,第三次握手是为了使得sever知道客户端答应了连接的请求。其中两次握手只能确定从客户端到服务端的网络是可达的,但却无法保证从服务端到客户端的网络是可达的。所以我们一定要保证双向的可达。

 如上图所示,没有第三次握手则不知道A的听筒是否正常,导致不能正常通话

TCP三次握手:

第一次握手:

TCP客户端打算建立连接,向服务器发送连接请求报文,客户端请求连接,客户端进行同步发送状态(SYN-SEND),在连接请求报文中,把SYN=1,表示这是一个请求连接报文,把序号字段seq=x(x就是一个初始值),作为客户端的初始序号

第二次握手:

TCP服务端在收到客户端的请求后,如果体连接,则会向客户端发送确认请求报文,服务器进入同步接收状态,在确认报文中,把同步位SYN和确认位ACK置为1 ,表示这是一个请求确认报文。把序号seq设置为一个初始值y,作为服务器的初始序号。把确认字段ack=x+1作为对客户端的确认

第三次握手:

TCP客户端在收到了服务器的确认信号后,还要向服务器发送一个确认报文,并进入连接已建立(ESTABLISHED),发送针对服务器确认的确认报文。

确认位ACK=1,表示这是一个确认报文

序号seq=x+1,表示第一次是x,第二次发送x+1

确认号ack=y+1,则是对服务器的确认

服务器收到后,进入连接已建立

 三、断开连接——四次挥手

   断开TCP连接共有四步,即四次挥手

第一次挥手:

TCP客户端主动关闭TCP连接,TCP客户端发送连接释放(断开)报文给服务端,并进入终止等待态1,

在连接断开释放报文中:

(1)终止位FIN和确认位ACK要设置为1,表示是一个TCP连接释放报文,同时对之前的报文做确认

(2)序号seq设置为u(表示特定的值),等于之前已经传送过去的数据最后一个字节+1

(3)确认号ack设置为v,等于之前收到的数据最后一个字节序号+1

第二次挥手:

TCP服务器收到了TCP连接断开请求报文,会发送一个确认报文给客户端,并进入关闭等待状态,客户端收到确认会进入终止等待态2

在断开确认报文中:

确认号ACK=1,表示是一个确认报文序号seq=v,等于之前服务器发送的最后一个字节+1,与次一次挥手客户端的确认号做匹配

继续把当前没有传输完成的数据传输完毕

第三次挥手:

TCP服务端给客户端发送连接释放报文,并进入最后确认状态

在服务端连接释放报文中:

终止位FIN和确认位ACK设置为1,表示这是一个TCP连接释放,同时对之前的数据做确认序号seq=w,这时服务器属于半关闭状态(断开是双向的)确认号ack=u+1,释放的重复确认

第四次挥手:

TCP客户端在收到服务端的连接释放,发送确认报文,并进入时间等待状态

确认位ACK=1,表示是一个确认报文

序号seq=u+1(上一次是u,再发一次),表示是一个释放报文

确认号ack=w+1,表示是一个确认

 TCP四次挥手为什么要等待2MSL?

一个MSL表示报文存活的最大时间,不管是A发送到B的报文,还是B发送到A的报文,都是最大可存活1MSL,那么等待2MSL,也就是报文一来一回。

四、socket编程

客户端:类比于打电话

类比

功能

函数

有手机

创建套接字(有对应的TCP协议)

socket()

有手机号码

绑定套接字(有自己的网络信息)

bind()

拨打对方电话号码

请求服务器连接(与服务器建立连接)

connect()

进行通话

收发数据(进行通信)

read()、write()、recv()、send()、

挂断电话

结束通信(关闭套接字)

close()

##客户端API函数

1.创建套接字

#include <sys/types.h>

#include <sys/socket.h>

//创建套接字文件,为进程添加一个对应的网络通信协议(文件),返回值就是创建号的文件描述符(socket描述符就代表一套协议---套接字)

int socket(int domain, int type, int protocol);

参数1:

<code>int domain:地址族,选用那种网络层协议地址

AF_INET------IPV4

AF_INET6-----IPV6

参数2:

int type:套接字类型

SOCK_STREAM--------TCP

SOCK_DGRAM---------UDP

SOCK_RAW:原始套接字(没有传输层协议)

参数3:

int protocol:套接字协议

 0:套接字默认协议

返回值:

int:整数---------文件描述符(套接字文件)

成功:返回套接字描述符 >= 0

失败:返回-1

2.绑定套接字

#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

//绑定本地网络信息到套接字中

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数1:

int sockfd:绑定本地网络信息到哪个套接字上(绑定到哪一套协议上)

参数2:

const struct sockaddr *addr:结构体地址,这个结构体中存储的本地网络信息(要绑定的网络信息ip、port)

struct sockaddr {//通用结构体,表示一个网络信息内容

sa_family_t sa_family;

char sa_data[14];

}

//IPV4 网络信息结构体

struct sockaddr_in {

sa_family_t sin_family;//地址族 AF_INET

in_port_t sin_port;//端口

struct in_addr sin_addr;//结构体变量--ip地址

};

/* Internet address. */

struct in_addr {

  uint32_t s_addr;//ipv4地址

};

参数3:

socklen_t addrlen:整数,结构体大小(确定信息结构体的大小)

返回值:

成功:返回0

失败:返回-1

3.请求连接

#include <sys/types.h>          

#include <sys/socket.h>

//请求与服务器建立连接

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数1:

int sockfd:客户端的套接字,使用套接字与服务端建立连接

参数2:

const struct sockaddr *addr:服务端的ip、prot信息,客户端要和哪个服务器建立连接

参数3:

socklen_t addrlen:结构体的大小

返回值:

成功:返回0

失败:返回-1

实现代码:

//TCP客户端进行通信(先发再收)

#include <netinet/in.h>

#include <arpa/inet.h>

#include <sys/types.h>

#include <netinet/in.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <stdio.h>

#include<stdlib.h>

#include<string.h>

int main()

{

//1、创建套接字,选择对应的网络通信协议组

int socketfd = socket(AF_INET,SOCK_STREAM,0);//选择TCP协议

if(socketfd < 0)

{

printf("socket create error\n");

return -1;

}

//2、绑定套接字,对套接字添加自己当前进行需要到本地网络信息

struct sockaddr_in clientaddr;//有一个IPV4网络信息结构体

clientaddr.sin_family = AF_INET;//地址族-----IPV4

clientaddr.sin_port = htons(20000);//为当前进程添加的端口号为10000

clientaddr.sin_addr.s_addr = inet_addr("0.0.0.0");

bind(socketfd,(struct sockaddr *)&clientaddr,sizeof(clientaddr));

//与服务器建立连接

struct sockaddr_in serveraddr;//服务端IPV4网络信息结构体

serveraddr.sin_family = AF_INET;//地址族-----IPV4

serveraddr.sin_port = htons(10000);//为当前进程添加的端口号为10000

serveraddr.sin_addr.s_addr = inet_addr("192.168.138.1");

if(connect(socketfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr))<0)

{

printf("connect error\n");

}else{

printf("connect ok\n");

}

char buf[20];

while(1)

{

memset(buf,0,20);//清空buf

fgets(buf,20,stdin);

write(socketfd,buf,20);//发送

memset(buf,0,20);

read(socketfd,buf,20);//接收

printf("data is %s\n",buf);

}

close(socketfd);

return 0;

}

服务端:类比于接电话

类比

功能

函数

有手机

创建套接字(有对应的TCP协议)

socket()

有手机号码

绑定套接字(有自己的网络信息)

bind()

等待电话(待机)

监听服务端套接字(查看是否有客户端连接)

listen()

接听电话

接收同意客户端的连接请求

accept()

进行通话

收发数据(进行通信)

read()、write()、recv()、send()、

挂断电话

结束通信(关闭套接字)

close()

##服务端API函数

1.创建套接字

#include <sys/types.h>

#include <sys/socket.h>

//创建套接字文件,为进程添加一个对应的网络通信协议(文件),返回值就是创建号的文件描述符(socket描述符就代表一套协议---套接字)

int socket(int domain, int type, int protocol);

参数1:

<code>int domain:地址族,选用那种网络层协议地址

AF_INET------IPV4

AF_INET6------IPV6

参数2:

int type:套接字类型

SOCK_STREAM--------TCP

SOCK_DGRAM---------UDP

SOCK_RAW:原始套接字(没有传输层协议)

参数3:

int protocol:套接字协议

 0:套接字默认协议

返回值:

int:整数---------文件描述符(套接字文件)

成功:返回套接字描述符 >= 0

失败:返回-1

2.邦定套接字

#include <sys/types.h> /* See NOTES */

#include <sys/socket.h>

//绑定本地网络信息到套接字中

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数1:

int sockfd:绑定本地网络信息到哪个套接字上(绑定到哪一套协议上)

参数2:

const struct sockaddr *addr:结构体地址,这个结构体中存储的本地网络信息(要绑定的网络信息ip、port)

struct sockaddr {//通用结构体,表示一个网络信息内容

sa_family_t sa_family;

char sa_data[14];

}

//IPV4 网络信息结构体

struct sockaddr_in {

sa_family_t sin_family;//地址族 AF_INET

in_port_t sin_port;//端口

struct in_addr sin_addr;//结构体变量--ip地址

};

/* Internet address. */

struct in_addr {

  uint32_t s_addr;//ipv4地址

};

参数3:

socklen_t addrlen:整数,结构体大小(确定信息结构体的大小)

返回值:

成功:返回0

失败:返回-1

3.监听

#include <sys/types.h>          

 #include <sys/socket.h>

监听等待客户端连接,如果有客户端的连接只会把客户端的连接存储起来(会创建一个监听队列),只要有客户端来进行连接,都会放在监听等待队列中——能够一直查看服务器自己的信息,是否有客户端连接。且之后套接字只能用于监听,不能用于与客户端进行通信

       int listen(int sockfd, int backlog);当调用后自动监听(查看是否有客户端连接)

参数1:

int sockfd:要进行监听的套接字,就是之前绑定了服务器ip、port的套接字,

表示要监听哪个套接字是否有客户端连接

参数2:

int backlog:最多同时能够存储多少个客户端连接——等待队列大小

返回值:

成功——  0     

失败——-1

4.同意连接请求

#include <sys/types.h>          /* See NOTES */

#include <sys/socket.h>

//服务器同意连接请求(从等待队列中取出一个客户端连接请求,建立连接。如果监听队列没有连接请求,就阻塞等待监听队列有连接请求)——一定要接收一个连接请求

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数1:

int sockfd:监听套接字,从哪个监听中取出连接

参数2:

struct sockaddr *addr:用于存储客户端ip,port,不需要写NULL

参数3:

socklen_t *addrlen:结构体大小

返回值:

成功——返回与连接成功的客户端通信的套接字   

失败——-1

5.发送或接收数据

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数1:

int sockfd:套接字

参数2:

const void *buf:要发送或接收的数据

参数3:

size_t len:数据大小

参数4:

int flags:标志,选项

0:阻塞

实现代码:

//tcp服务端,与客户端进行通信

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <string.h>

#include <stdio.h>

#include <unistd.h>

int main()

{

//1、创建套接字

int sockfd = socket(AF_INET,SOCK_STREAM,0);//在服务器创建TCP套接字

//2、绑定套接字

struct sockaddr_in serveraddr;

serveraddr.sin_addr.s_addr = inet_addr("192.168.124.80");

serveraddr.sin_port = htons(9999);

serveraddr.sin_family = AF_INET;

bind(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));

//3、监听套接字

listen(sockfd,10);

//4、接受客户端连接

int clientfd = accept(sockfd,NULL,NULL);//返回值就是与客户端通信的套接字

printf("ok\n");

//服务器与客户端如何进行通信

char buf[50];

while(1)

{

printf("recv\n");

sleep(10);

memset(buf,0,50);

int num = recv(clientfd,buf,50,0);//返回值就是接收的大小

if(strcmp(buf,"quit\n") == 0)

break;

printf("size is %d; data is %s",num,buf);

send(clientfd,buf,50,0);

printf("send ok\n");

}

close(clientfd);

close(sockfd);

return 0;

}



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。