文章目录
TCP如何保证可靠传输
1、确认应答机制
发送的每条数据都需要确认回复一下
2、超时重传机制
发送方等待一段时间后还没有收到回复,就认为传输失败了,将数据重传。这个超时时间是递增的,次数有限,超过重传次数就认为网络断开了。
3、序号/确认序号------保证数据有序传输
4、反馈重发
(一)停等协议:发送数据之后,我就停下来等待回复报文,回复报文为确认帧(ACK),就发送下一条数据,回复报文是NAK,就自动重新发送该段报文。
(二)拉回方式(Go_Back_To_N):第N条数据出错,将第N条数据和第N条数据之后的数据都重新发送。
(三)选择重发:丢掉那条数据重新发送那条数据。 代价:需要接收端由足够大的接受缓冲区来存储出错数据之后的正确数据。比如第N条数据出错了,我现在已经接收到第N+100条数据了,那么我只需要对端重传第N条数据,那么第N条数据之后的100条数据都要存到我的接受缓冲区中去,因为接受数据只能按序号接受,并且选择重发实现起来存在更大的技术困难,因此相比较拉回方式很少使用选择重传。
TCP提升性能机制
tcp因为要保证可靠传输,因此性能有很大的消耗,为了提高tcp传输性能,有需要有其他的一些机制。
1、流量控制
通过不断设定窗口大小来告诉对方我能一次接收多少数据,最终达到流量控制的效果,避免因为发送太快,而处理太慢,导致接收方缓冲区塞满引起的大量的数据宝重传。
2、快速重传
一次发送多条数据,集中等待(快速重传):如果发送方没有接收到第一条报文的ACK1,但是收到了第二条数据的ACK2,那么他认为第一条也发送成功了,不需要重传,假如第一条数据丢了,那么接受方会连续发送3次ACK1说要的是第一条数据,但是并不会发送第二条已经收到的ACK2,发送方接收到重传请求就会进行数据重传。一次发送的数据有多少取决于协议中的窗口大小字段----窗口大小在进行数据传输前会进行协议。窗口大小实际也决定了TCP的传输吞吐量。
3、拥塞控制
一开始的时候,因为窗口比较大,发送方一次发送的数据比较多,但是网络状况不好,就有可能会大量丢包,丢包就需要重传,造成性能下降。
双方进行窗口大小协商之后,发送方还有一个东西叫拥塞窗口,并且拥塞窗口大小一开始的时候非常小,而实际发送大小取小的那一个,也就意味着拥塞窗口控制了tcp传输的一个慢启动,快速增长,一旦丢包,回到很低的窗口位置,再次轮回。
4、延时应答机制
对处理速度比较自信,对接收到的数据能够快速处理,只要稍微延时一下进行ACK应答,那么能够设置的窗口大小就会尽可能的大,一直保证一个tcp的最大吞吐量,假如是立即应答,那么有可能回复的这个窗口大小就会比较小。
5、捎带应答机制
将确认应答直接标记在即将发送的数据包内,那么这样不仅传输了数据还对上一次接受的数据进行应答(少传输一个应答包)
可同时处理多个TCP请求的服务器
多进程tcp服务端程序
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
int create_worker(int cli_fd)
{
int pid = fork();
if (pid < 0)
{
return -1;
}
else if (pid == 0)
{
while(1)
{
char buff[1024] = {0};
int ret = recv(cli_fd, buff, 1024, 0);
if (ret <= 0) {
close(cli_fd);
exit(-1);
}
printf("client say:[%s]\n", buff);
send(cli_fd, "what\?\?!!", 8, 0);
}
}else {
close(cli_fd);
}
return 0;
}
void sigcb(int signo)
{
//如果返回值大于0,代表有子进程退出,我们不确定现在到底有几个子进程
//退出了,所以就一直循环,直到返回值为0位置,0就代表没有子进程退出
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[])
{
signal(SIGCHLD, sigcb);
//1. 创建监听socket(这个socket仅仅用于接收客户端的连接请求)
//struct socket ->>>struct file
int lst_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (lst_fd < 0) {
perror("socket error");
return -1;
}
//2. 为监听socket绑定地址信息(如果使用socket发送数据,则是从绑定的
//这个地址和端口发送数据,)
//htons--将两个字节的数据转换为网络字节序的数据
//htonl--将四个字节的数据转换为网络字节序的数据
//ntohs--将两个字节的网络字节序数据转换为当前的主机字节序数据
//ntohl--将四个字节的网络字节序数据转换为当前的主机字节序数据
struct sockaddr_in lst_addr;
lst_addr.sin_family = AF_INET;
lst_addr.sin_port = htons(9000);
lst_addr.sin_addr.s_addr = inet_addr("192.168.122.132");
socklen_t len = sizeof(struct sockaddr_in);
int ret = bind(lst_fd, (struct sockaddr*)&lst_addr, len);
if (ret < 0) {
perror("bind error");
return -1;
}
//3. 开始监听
// listen会在系统里边开辟一个队列,第二个参数就是定义这个队列的最大
// 节点数,这个队列名字叫做连接成功队列
// 第二个参数的功能:同一时间socket的最大并发连接数,记住不是最大连
// 接数
//
// 一旦socket开始监听,那么就可以开始接收客户端的连接请求,
// 如果有客户端的连接请求过来,操作系统会为这个客户端新建一个socket
// 连接建立成功后这个socket将被放入连接成功队列
// 接下来如果想要和这个客户端进行通信,那么就需要把这个socket从队列
// 中取出来,进行操作
if (listen(lst_fd, 5) < 0) {
perror("listen error");
return -1;
}
while(1) {
//4. 从连接成功队列中,取出为客户端新连接创建的socket,与客户端
//的数据通信都是通过这个socket来完成的。
// accept是一个阻塞型的函数,如果连接成功队列中没有新的socket
// 那么它就一直阻塞在这里了,直到有客户端连接上来创建socket
struct sockaddr_in cli_addr;
int cli_fd = accept(lst_fd, (struct sockaddr*)&cli_addr, &len);
if (cli_fd < 0) {
perror("accept error");
continue;
}
create_worker(cli_fd);
}
close(lst_fd);
return 0;
}
多线程tcp服务端程序
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void *thr_start(void *arg)
{
int clifd = (int)arg;
while(1) {
char buff[1024] = {0};
ssize_t ret = recv(clifd, buff, 1024, 0);
if (ret < 0) {
perror("recv error");
close(clifd);
break;
}else if (ret == 0) {
printf("peer shutdown!!\n");
}
printf("client say:[%s]\n", buff);
send(clifd, "what!!", 6, 0);
}
return NULL;
}
int create_worker(int clifd)
{
//创建一个线程来单独处理与指定客户端的通信
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, thr_start, (void*)clifd);
if (ret != 0) {
printf("create pthread error\n");
return -1;
}
return 0;
}
int main(int argc, char *argv[])
{
int lstfd, clifd, ret;
socklen_t len;
struct sockaddr_in lst_addr;
struct sockaddr_in cli_addr;
if (argc != 3) {
// ./ttcp_server 192.168.122.132 9000
printf("Usage: ./ttcp_server ip port\n");
return -1;
}
lstfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (lstfd < 0) {
perror("socket error");
return -1;
}
lst_addr.sin_family = AF_INET;
lst_addr.sin_port = htons(atoi(argv[2]));
lst_addr.sin_addr.s_addr = inet_addr(argv[1]);
len = sizeof(struct sockaddr_in);
ret = bind(lstfd, (struct sockaddr*)&lst_addr, len);
if (ret < 0) {
perror("bind error");
return -1;
}
if (listen(lstfd, 5) < 0) {
perror("listen error");
return -1;
}
while(1) {
clifd = accept(lstfd, (struct sockaddr*)&cli_addr, &len);
if (clifd < 0) {
perror("accept error");
continue;
}
create_worker(clifd);
}
return 0;
}