关闭

浅析TCP通信原理

标签: TCP通信socket三次握手四次挥手
293人阅读 评论(0) 收藏 举报
分类:
TCP通信
TCP是面向连接的通信,所以在通信之前,客户端与服务器端必须通过三次握手建立连接,然后在通信完毕,还要通过四次挥手断开连接。
(一)相关函数
1.创建套接字

domain:地址类型,ipv4ipv6unix的地址类型分别定义为常数AF_INETAF_INET6AF_UNIX.
type:socket传输类型,tcp通信是面向字节流的,所以为SOCK_STREAM

在网络通信时,我们的数据要从本主机通过网络发送到对端主机,数据在内存中存放的形式有大端或者小端两种形式,所以在向网络中传输数据是,网络就要按照一定的规定收发数据。TCP/IP协议规定,网络字节流应按照大端字节流,即低地址高字节。
网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高低址,因为网络字节流为大端,也就是先发送数据的高位字节,在发送低位字节。

2.为了代码的可移植性,下面库函数为实现网络字节序列到主机字节序列的转换。

h = host主机
n = network网络
l = 长整形
s = 短整形
htonl代表主机字节序转换成网络字节序

3.将套接字与socket结构体绑定,socket结构体会指定ip,端口号,还有地址类型

一般服务器的端口号和ip是绑定的,众所周知的,而客户端的端口号可以随机分配的
IPV4 struct sockaddr_in 结构体的结构
IP地址+端口就称为socket,所以socket的结构体内包含有ip 和端口号
struct in_addr {
__be32 s_addr;
};
struct sockaddr_in {
sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
/* Pad to size of `struct sockaddr'. */
unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -
sizeof(unsigned short int) - sizeof(struct in_addr)];
};
sin_family:地址类型,ipv4ipv6unix的地址类型分别定义为常数AF_INETAF_INET6AF_UNIX.
地址类型的作用:因为在不同的环境下socket的数据结构不同,ipv4使用的struct sockaddr_in,而ipv6使用的是struct sockaddr_in6。但在网络编程里的许多函数需要传参sockaddr结构体,Struct sockaddr结构体类型就如同void*类型,它可以接受任意类型的结构体,所以在传参时就不需要知道具体的socket结构体类型,可以给根据地址类型来确定结构体的内容。
Sockeaddr数据结构

4.将sockfd设置为监听套接字,并通过参数2是指明最多可以监听多少个套接字。

监听套接字的作用:server服务器启动后,会源源不断的有客户端来连接,这时候就需要一个监听套接字,来把一个个来访的socket按顺序存起来,并按顺序交给accept的去处理并返回newsockfd去收发数据,这样做可以保证在server满负荷的处理其他socket时,其他客户端要访问服务器时,可以通过监听队列等待一会,有其他客户端断开连接了,他就可以连接了。

5.

阻塞式等待客户端连接,监听套接字一直在监听是否有新连接到来连接,如果有链接则接受对方连接,连接之后由返回值new_sock收发数据
new_sock(1) = accept(监听套接字(2),struct socket_in输出型参数(3),输入输出型参数(4))
6.

client不需要被别人连接,只需要连接别人,所以使用connect来连接服务器

(二)TCP通信的基本原理

 
三次握手建立连接:server端调用socket(),bind(),listen()创建监听套接字并完成初始化,然后调用accept()阻塞式等待客户连接。客户端创建一个套接字初始化后,调用connect连接server,连接过程:调用connect()发出SYN段并阻塞等待服务器应答(client:我想要连接你),服务器应答一个SYN-ACK(server:好,我准备好了,你连接吧),客户端收到从connect()返回,同时应答一个ACK给server(client:太好了,我连接好了),服务器收到ACK,从accept返回。

数据传输过程:建立连接后,TCP可以提供全双工的通信,server先读再写,client先写在读,用read()和write()阻塞式的等待一个写一个读。一直循环下去。

四次挥手关闭连接:假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,"告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息"。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,"告诉Client端,好了,我这边数据发完了,准备好关闭连接了"。Client端收到FIN报文后,"就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,"就知道可以断开连接了"。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!如果一方调用shutdown()则连接处于半关闭状态,仍可接受对方的数据。

(三)代码实现
server.c
clude<stdio.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define _BACKLOG_ 10

int GetSocket(int port)
{
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock<0){
        perror("socket");
        exit(1);
    }
    printf("%d:socket create is ok\n", sock);
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    server.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY这个宏代表本地的任意ip地址,
                                                                                //因为本地可能多个网卡。这个宏的值为0
    if(bind(sock, (struct sockaddr*)&server,sizeof(server))<0){
        perror("bind");
        close(sock);
        exit(2);
    }
    printf("bind is ok\n");
    if(listen(sock, _BACKLOG_)< 0){
        perror("listen");
        close(sock);
        return 3;
    }
    printf("listen is ok\n");
    return sock;
}


void use(char *a)
{
    printf("#%s [port_server]\n", a);
}
int main(int argc, char *argv[])
{
    printf("main start\n");
    if(argc<2)
    {
       use(argv[0]);
       return 6;
    }
    printf("use is ok\n");
    int listen_sock = GetSocket(atoi(argv[1])); 
    
    printf("GetSocket is ok\n");
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    
    printf("wait accept....\n");
    while(1)
    {
        int new_sock = accept(listen_sock,(struct sockaddr*)&client, &len);
        if(new_sock< 0)
        {
            perror("accept");
            close(new_sock);
            return 4;
        }
        printf("[%s][%d]:accept is ok\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
        pid_t pid = fork();
        if(pid < 0){
            close(new_sock);
            printf("process creation failed\n");
            continue;
        }else if(pid == 0){
            close(listen_sock);
            if(fork()>0){//fork()两次,使得孙子进程变成孤儿进程受init回收,父进程直接回收子进程,解决父进程阻塞问题
            	exit(2);
            }
            else{
            while(1)
            {   
            fflush(stdout);
            char buf[1024];
            ssize_t i = read(new_sock, buf, sizeof(buf));
            if(i>0){
            printf("[%s][%d]:client say#%s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
            }else if(i == 0){
                close(new_sock);
                printf("[%s][%d]:client goodbye\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
                break;         
            }else{
                perror("read");
                break;
            }
            printf("Enter to [%s][%d]#",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
            fflush(stdout);
            fgets(buf, sizeof(buf), stdin);
            buf[strlen(buf)-1] = '\0';
            write(new_sock, buf, strlen(buf));
            }
            }
        }else{
            close(new_sock);
            waitpid(pid,NULL,0);
        }
    }
    close(listen_sock);
    return 0;
}


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


void use(char *argv)
{
    printf("#%s [ip_server] [port_srvera]\n", argv);
}
int main(int argc, char *argv[])
{
    printf("main start\n");
    if(argc<3)
    {
        use(argv[0]);
        return 3;
    }
    printf("use is ok\n");
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock<0){
       perror("socket");
       return 1;
    }      
    printf("create socket is ok\n");
    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]));
    int conn = connect(sock, (struct sockaddr*)&server, sizeof(server));  
    if(conn<0){
        perror("connect");
        close(sock);
        return 2;
    }
    while(1)
    {
        printf("please enter#");
        fflush(stdout);
        char buf[1024];
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf)-1] = '\0';
        write(sock, buf, sizeof(buf));
        char* str = "quit";
        if(strcmp(buf, str)==0){
             break;
        }
        printf("server echo#");
        fflush(stdout);
        ssize_t r2 = read(sock, buf, sizeof(buf));
        if(r2>0) {
            printf("%s\n",buf);
        }else{
            
            continue;
        }
    }
    close(sock);
    printf("client goodbye!!!\n");
    return 0;
}


(四)存在问题:
但是在运行的时候发现一个问题,server启动后,然后启动client建立连接后,然后直接ctrl+c终止掉server,无法立即重启,必须等待半分钟才能重新启动。如下图

这是因为在四次挥手断开连接时,主动断开的一方会进入TIME_WAIT状态,这是server还没有完全断开连接,还占着8080号端口,所以再次启动时创建监听套接字就无法在绑定上8080号端口。
但是这是不合理的,因为在实际生活中,服务器一旦挂了,不能立即重启,可能会影响许多客户的体验,会造成很大的损失,那么如何解决。
解决:

解决这个问题的方法是使用setsockopt()设置socket描述符的,设置选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。在server代码的socket()和bind()之间插入。
4
1

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:22228次
    • 积分:911
    • 等级:
    • 排名:千里之外
    • 原创:62篇
    • 转载:12篇
    • 译文:0篇
    • 评论:24条
    个人专属
    文章分类
    最新评论