传输层——TCP协议

1 TCP协议报头

源端口号、目的端口号:标识数据从哪里来,要发送到哪里去
4位首部长度:TCP报头长度,用于分离报头信息和有效载荷
                      4个比特位最大值为15,以四字节为单位(15*4 = 60),TCP报头长度20-60字节
6位保留位:现在还没有具体用处,方便以后去扩展
16位窗口大小:进行流量控制
16位紧急指针:标识那部分数据是紧急数据。优先处理数据的偏移量(优先处理的数据也称为“带外数据”)
选项:用于对报头信息的扩展

2 序号和确认序号

序号m:是指给发送数据的编号m
确认序号n:是指编号为n以前的数据都收到了,下次发送数据可以从n开始发送
序号和确认序号的作用:
(1)提供TCP的确认机制和重传机制;(确认机制:确认序号确认数据收到,重传机制:若发送数据的序号为1000但确认序号为1000,则说明编号为1000的数据并未收到,需要重传)
(2)保证数据的按序到达;(序号表明数据发送的顺序,确认序号依照序号的顺序)
(3)可以批量化的处理一堆报文;(发送数据的序号分别为:1000、1001、1002、1003,确认序号只有:1004,则表明1004以前的数据全部收到)
(4)数据去重。(若发送端A重复发送数据1000,接收端B发现自己已经确认过1000了,便不再确认,直至有未确认的数据到达时才确认)
为什么要有序号和确认序号一对序号,只有一个序号不可以吗?
不可以,因为TCP是全双工的,主机A、B间需要互发数据,只有一个序号无法做到。

3 6位标志位
URG:紧急指针标记位。置1,表明报头中的“紧急指针”有效,该报文需要优先处理
ACK:表明确认序号是否有效。一般来说,除建立连接请求的报文外,其余报文都需设置该位
PSH:提示接收端需要马上把缓冲区的内容读走。当接收端缓冲区快满的时候,发送端提示接收端将缓冲区中数据读走,否则发送端可能无法发送给数据
RST:对方请求重新建立连接。一般为“三次握手”过程中未建立好连接
SYN:请求建立连接。
FIN:请求断开连接,通知对方本端要关闭了。

4 TCP协议特点
(1)面向连接:有建立连接的“三次握手”和“四次挥手”
(2)可靠:有确认机制和重传机制等保证TCP的可靠性
(3)面向字节流:读取报文不用一个报文一个报文长度的读取,可以任意读取
(4)全双工:发送端和接收端双方可以同时通信
【注】:TCP既有发送缓冲区也有接收缓冲区

5 TCP的粘包问题
粘包问题是指由于TCP是面向字节流的,发送和接收时不需要一个报文一个报文的进行,所以在应用层拿到数据时就会是一串连续的字符串,不知道哪里是一个数据包的开始,哪里是该数据包的结尾。

如何解决粘包问题?
解决粘包问题的关键在于明确两个包之间的边界!!!
(1)采用定长报文的方式,每次都按照固定的长度读取,如:UDP协议,在报头中有报头的长度
(2)采用自描述字段,如:HTTP协议报头中的Content_Length字段,在报头位置告诉报文的长度
(3)采用明显的分割符,如:HTTP协议报头与报文中的空行

6 编写TCP——socket套接字
TCP服务器,实现功能:接收客户端数据,并将数据回显给客户端
单进程版本TCP服务器:有多个连接时会被阻塞
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SIZE 128

int startup(int port,char* ip)
{
    //创建套接字
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        printf("socket error!\n");
        return 2;
    }

    //填充本地套接字
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = inet_addr(ip);

    //绑定端口号
    if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        printf("bind error!\n");
        return 3;
    }

    //设置为监听状态
    if(listen(sock,5) < 0)
    {
        printf("listen error!\n");
        return 4;
    }

    return sock;
}

void service(int sock,int port,char* ip)
{
    char buf[SIZE];
    while(1)
    {
        buf[0] = 0;
        ssize_t s = read(sock,buf,sizeof(buf)-1);

        if(s > 0)
        {
            buf[s] = 0;
            printf("[%s:%d] say: %s\n",ip,port,buf);
            write(sock,buf,strlen(buf));
        }
        else if(s == 0)//read返回值为0,表示读到文件结束,client关闭
        {
            printf("client [%s:%d] quit!\n",ip,port);
            break;
        }

        else
        {
            printf("read error!\n");
            break;
        }
    }
}

int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:%s [ip][port]\n",argv[0]);
        return 1;
    }

    int listen_sock = startup(atoi(argv[2]),argv[1]);

    struct sockaddr_in peer;//远端
    
    while(1)
    {
        socklen_t len = sizeof(peer);
        //获得新的链接accept
        int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
        if(new_sock < 0)
        {
            printf("accept error!\n");
            continue;
        }

        //链接成功,显示链接的ip地址和端口号
        printf("get new connect,[%s:%d]\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
        service(new_sock,ntohs(peer.sin_port),inet_ntoa(peer.sin_addr));

        close(new_sock);
    }
    return 0;
}
TCP客户端,实现功能:发送数据,显示服务器回显数据,实现简单的客户端——服务器通信
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SIZE 128

int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:%s [ip][port]\n",argv[0]);
        return 1;
    }

    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        printf("client sock error!\n");
        return 2;
    }

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);

    //向服务器发起链接connect
    socklen_t len = sizeof(server);
    int ret = connect(sock,(struct sockaddr*)&server,len);
    if(ret < 0)
    {
        printf("connect error!\n");
        return 3;
    }

    printf("connect success!\n");

    char buf[SIZE];
    while(1)
    {
        printf("please enter#");
        fflush(stdout);
        ssize_t s = read(0,buf,sizeof(buf)-1);
        if(s > 0)
        {
            buf[s-1] = 0;
            if(strcmp("quit",buf) == 0)
            {
                printf("client quit!\n");
                break;
            }
            write(sock,buf,strlen(buf));

            s = read(sock,buf,sizeof(buf)-1);
            buf[s] = 0;
            printf("server echo# %s\n",buf);
        }
    }

    close(sock);

    return 0;
}
结果显示:

服务器不可能只连接一个客户端,所以单进程版本的TCP服务器在实际使用时无法使用,因此有多进程和多线程版本的TCP服务器,实现同时处理多个链接请求。

多进程TCP服务器:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SIZE 128

int startup(int port,char* ip)
{
	//创建套接字
	int sock = socket(AF_INET,SOCK_STREAM,0);
	if(sock < 0)
	{
		printf("socket error!\n");
		return 2;
	}

	//填充本地套接字
	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(port);
	local.sin_addr.s_addr = inet_addr(ip);

	//绑定端口号
	if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
	{
		printf("bind error!\n");
		return 3;
	}

	//设置为监听状态
	if(listen(sock,5) < 0)
	{
		printf("listen error!\n");
		return 4;
	}

	return sock;
}

void service(int sock,int port,char* ip)
{
	char buf[SIZE];
	while(1)
	{
		buf[0] = 0;
		ssize_t s = read(sock,buf,sizeof(buf)-1);

		if(s > 0)
		{
			buf[s] = 0;
			printf("[%s:%d] say: %s\n",ip,port,buf);
			write(sock,buf,strlen(buf));
		}
		else if(s == 0)
		{
			printf("client [%s:%d] quit!\n",ip,port);
			break;
		}

		else
		{
			printf("read error!\n");
			break;
		}
	}
}

int main(int argc,char* argv[])
{
	if(argc != 3)
	{
		printf("Usage:%s [ip][port]\n",argv[0]);
		return 1;
	}

	int listen_sock = startup(atoi(argv[2]),argv[1]);

	struct sockaddr_in peer;//远端
	
	while(1)
	{
		
		socklen_t len = sizeof(peer);
		//获得新的链接accept
		int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
		if(new_sock < 0)
		{
			//printf("accept error!\n");
			perror("accept");
			continue;
		}

		//链接成功,显示链接的ip地址和端口号
		printf("get new connect,[%s:%d]\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));

		//多进程版本:父进程接受新链接,子进程提供服务
		//为防止子进程变为僵尸进程,创建孙子进程,让孙子进程提供服务,
		//这样即使子进程变为僵尸态,孙子进程会被1号进程领养,有效防止回收问题,
		//同时此时父进程和孙子进程没有等待的关系,可以有效的使两个进程并行
		pid_t id = fork();
		if(id == 0)//child
		{
			close(listen_sock);//子进程不需要listen_sock
			if(fork() > 0)
				exit(0);
	
			service(new_sock,ntohs(peer.sin_port),inet_ntoa(peer.sin_addr));
			close(new_sock);
			exit(0);
		}

		else if(id > 0)//father
		{
			close(new_sock);//父进程不需要new_sock,节省文件描述符资源
			waitpid(id,NULL,0);
		}

		else
		{
			printf("fork error!\n");
			continue;	
		}
	}
	return 0;
}

多线程TCP服务器:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SIZE 128

typedef struct Arg
{
	int sock;
	int port;
	char addr[24];
}Arg;

int startup(int port,char* ip)
{
	//创建套接字
	int sock = socket(AF_INET,SOCK_STREAM,0);
	if(sock < 0)
	{
		printf("socket error!\n");
		return 2;
	}

	//填充本地套接字
	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(port);
	local.sin_addr.s_addr = inet_addr(ip);

	//绑定端口号
	if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
	{
		printf("bind error!\n");
		return 3;
	}

	//设置为监听状态
	if(listen(sock,5) < 0)
	{
		printf("listen error!\n");
		return 4;
	}

	return sock;
}

void service(int sock,int port,char* ip)
{
	char buf[SIZE];
	while(1)
	{
		buf[0] = 0;
		ssize_t s = read(sock,buf,sizeof(buf)-1);
		if(s > 0)
		{
			buf[s] = 0;
			printf("[%s:%d] say: %s\n",ip,port,buf);
			write(sock,buf,strlen(buf));
		}
		else if(s == 0)
		{
			printf("client [%s:%d] quit!\n",ip,port);
			break;
		}

		else
		{
			printf("read error!\n");
			break;
		}
	}
}

void* pthread_run(void* ptr)
{
	Arg* arg = (Arg*)ptr;
	service(arg->sock,arg->port,arg->addr);

	close(arg->sock);
	free(arg);
}

int main(int argc,char* argv[])
{
	if(argc != 3)
	{
		printf("Usage:%s [ip][port]\n",argv[0]);
		return 1;
	}

	int listen_sock = startup(atoi(argv[2]),argv[1]);

	struct sockaddr_in peer;//远端
	//char* ipbuf = &peer.sin_addr;

	while(1)
	{
		socklen_t len = sizeof(peer);
		//获得新的链接accept
		int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
		if(new_sock < 0)
		{
			printf("accept error!\n");
			continue;
		}

		//链接成功,显示链接的ip地址和端口号
		printf("get new connect,[%s:%d]\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));

		pthread_t tid;
		Arg* arg =(Arg*)malloc(sizeof(Arg));
		arg->port = ntohs(peer.sin_port);
		arg->sock = new_sock;
		strcpy(arg->addr,inet_ntoa(peer.sin_addr));

		pthread_create(&tid,NULL,pthread_run,(void*)arg);
		pthread_detach(tid);	

		//service(new_sock,ntohs(peer.sin_port),inet_ntoa(peer.sin_addr));
	}
	return 0;
}

多进程和多线程版本TCP服务器比较

(1)多进程和多线程服务器都同时可以处理多个客户端请求;

(2)两者都编写简单;

(3)多进程服务器在连接到达之后才创建进程,需要时间,性能受损;

多线程服务器也存在同样的问题,但创建线程需要的时间比进程短,所以比多进程模式稍微好一点;

(4)随着进程的增多,CPU调度的压力增大,客户端等待的时间增加,影响性能;

多线程也存在相同的问题,但比进程稍好些;

(5)多进程服务器每个进程占用资源,而系统的资源又上限,所以只能服务有限个进程;

多线程同样,只不过线程占用的资源比进程少;

(6)多进程服务器稳定性强,一个进程异常不会影响其他进程;

多线程服务器稳定性差,有可能因为一个线程出现异常而导致整个服务器崩掉。




猜你喜欢

转载自blog.csdn.net/weixin_39294633/article/details/80955499