Linux网络编程系列之TCP协议编程

一、什么是TCP协议

        TCP(Transmission Control Protocol)协议是一种面向连接的、可靠的、基于字节流的传输控制协议,属于传输层。TCP协议可以通过错误检测、重传丢失的数据包、流量控制、拥塞控制等方式来实现可靠传输,同时也具有较好的效率和速度。

二、特性

        1、面向连接:TCP协议是一种面向连接的协议,需要在数据传输前先建立连接,传输完成后再释放连接。

        2、 可靠传输:TCP协议通过序号和确认机制来实现可靠传输,确保数据在网络中被正确无误地传输。

        3、流量控制:TCP协议通过窗口机制来实现流量控制,避免发送方发送过多数据导致接收方不堪重负。

        4、拥塞控制:TCP协议通过拥塞窗口控制、快速重传和快速恢复等机制来实现拥塞控制,避免网络拥塞导致数据传输的延迟和丢失。

        5、面向字节流:TCP协议是一种面向字节流的协议,数据被视为一连串的字节流进行传输。

        6、可以提供全双工传输:TCP协议可以同时接受发送方和接收方的数据传输,实现全双工传输。

        7、支持多路复用:TCP协议可以通过端口号来实现多路复用,使得多个应用程序可以同时使用同一个网络连接。

三、使用场景

        1、 网页浏览:网页浏览器使用TCP协议与Web服务器建立连接,以可靠地传输HTTP请求和响应数据。

        2、电子邮件传输:电子邮件客户端和邮件服务器之间的传输也使用TCP协议,以确保邮件的正确传输和接收。

        3、文件传输:文件传输协议(FTP)和远程拷贝协议(SCP)等协议均使用TCP协议进行可靠传输。

        4、数据库访问:数据库客户端通过TCP协议与数据库服务器建立连接,以进行数据的可靠读写操作(例如注册或者登录账号)。

        5、远程登录:远程登录协议(例如SSH)使用TCP协议进行可靠传输和安全身份验证。

        6、实时通信:实时通信协议(例如视频会议、语音聊天等,但一般使用UDP协议的多)也可以使用TCP协议进行可靠的数据传输。

        总之,任何需要可靠传输和连接保持的应用场景都可以使用TCP协议进行数据传输。

四、C/S架构TCP通信流程

        1、客户端

        (1)、建立套接字。使用socket()

        (2)、设置端口复用(可选,推荐)。使用setsockopt()

        (3)、绑定自己的IP地址和端口号(可以省略)。使用bind()

        (4)、向服务器发起连接请求。使用connect(),相当于第一个握手

        (5)、成功连接后可以进入收发数据。使用send()或者write()发送数据,recv()或者read()接收数据

        (6)、关闭套接字。使用close()

        2、服务端

        (1)、建立套接字。使用socket()

        (2)、设置端口复用(可选,推荐)。使用setsockopt()

        (3)、绑定自己的IP地址和端口号(不可以省略)。使用bind()

        (4)、设置监听。使用listen()

        (5)、接受连接请求。使用accept()

        (6)、成功连接后可以进入收发数据。使用send()或者write()发送数据,recv()或者read()接收数据

        (7)、关闭套接字。使用close()

Linux网络编程系列之TCP协议编程_第1张图片

五、相关的函数API

        1、建立套接字

// 建立套接字 
int socket(int domain, int type, int protocol);

// 接口说明
        返回值:成功返回一个套接字文件描述符,失败返回-1

        参数domain:用来指定使用何种地址类型,有很多,具体看别的资源
            (1)PF_INET 或者 AF_INET 使用IPV4网络协议
            (2)其他很多的,看别的资源

        参数type:通信状态类型选择,有很多,具体看别的资源
            (1)SOCK_STREAM    提供双向连续且可信赖的数据流,即TCP
            (2)SOCK_DGRAM     使用不连续不可信赖的数据包连接,即UDP
    
        参数protocol:用来指定socket所使用的传输协议编号,通常不用管,一般设为0

         2、设置端口状态

// 设置端口的状态
int setsockopt(int sockfd, 
               int level, 
               int optname,
               const void *optval, 
               socklen_t optlen);


// 接口说明
        返回值:成功返回0,失败返回-1
        参数sockfd:待设置的套接字

        参数level: 待设置的网络层,一般设成为SOL_SOCKET以存取socket层

        参数optname:待设置的选项,有很多种,具体看别的资源,这里讲常用的
            (1)、SO_REUSEADDR    允许在bind()过程中本地地址可复用,即端口复用
            (2)、SO_BROADCAST    使用广播的方式发送,通常用于UDP广播
            (3)、SO_SNDBUF       设置发送的暂存区大小
            (4)、SO_RCVBUF       设置接收的暂存区大小

        参数optval:待设置的值

        参数optlen:参数optval的大小,即sizeof(optval)

        3、绑定自己的IP地址和端口号

// 绑定自己的IP地址和端口号
 int bind(int sockfd, 
          const struct sockaddr *addr,
          socklen_t addrlen);

// 接口说明
        返回值:
        参数sockfd:待绑定的套接字

        参数addrlen:参数addr的大小,即sizeof(addr)

        参数addr:IP地址和端口的结构体,通用的结构体,根据sockfd的类型有不同的定义
        当sockfd的domain参数指定为IPV4时,结构体定义为
            struct sockaddr_in
            {
                unsigned short int sin_family;    // 需与sockfd的domain参数一致
                uint16_t sin_port;            // 端口号
                struct in_addr sin_addr;      // IP地址 
                unsigned char sin_zero[8];    // 保留的,未使用
            };
            struct in_addr
            {
                uin32_t s_addr;
            }
// 注意:网络通信时,采用大端字节序,所以端口号和IP地址需要调用专门的函数转换成网络字节序
    

        4、字节序转换接口 

// 第一组接口
// 主机转网络IP地址,输入主机IP地址
uint32_t htonl(uint32_t hostlong);

// 主机转网络端口,输入主机端口号
uint16_t htons(uint16_t hostshort);    // 常用

// 网络转主机IP,输入网络IP地址
uint32_t ntohl(uint32_t netlong);

// 网络转主机端口,输入网络端口
uint16_t ntohs(uint16_t netshort);


// 第二组接口,只能用于IPV4转换,IP地址
// 主机转网络
int inet_aton(const char *cp, struct in_addr *inp);

// 主机转网络
in_addr_t inet_addr(const char *cp);    // 常用

// 网络转主机
int_addr_t inet_network(const char *cp);

// 网络转主机
char *inet_ntoa(struct in_addr in);    // 常用

        5、设置监听

// 设置监听
int listen(int sockfd, int backlog);

// 接口说明
        返回值:成功返回0,失败返回-1
        参数sockfd:待监听的套接字
        参数backlog:指定同时能出处理的最大连接请求数量
            当sockfd的domain参数指定为AF_INET(IPV4)时,该参数backlog最大值为128

        6、向服务器发起连接请求 

// 向服务器发起连接请求
int connect(int sockfd, 
            const struct sockaddr *addr,
            socklen_t addrlen);

// 接口说明
        返回值:
        参数sockfd:成功返回0,失败返回-1
        参数addr:服务器的网络地址
        参数addrlen:参数addr的大小,即sizeof(addr)

        7、接收连接请求

// 接受连接请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// 接口说明
        返回值:成功返回一个新连接上来的客户端套接字文件描述符,失败返回-1
        参数sockfd:服务端套接字
        参数addr:连接上来的客户端的网络地址
        参数addrlen:参数addr的大小,即sizeof(addr)

        8、发送数据

// 发送数据
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

// 接口说明
        返回值:成功返回成功发送的字节数,失败返回-1
        参数sockfd:发送者的文件描述符
        参数buf:数据缓冲区
        参数len:发送的数据大小
        参数flags:有很多种,一般设置为0


// 发送数据,也可以使用这个
ssize_t write(int fd, const void *buf, size_t count);

// 接口说明
        返回值:成功返回成功发送的字节数,失败返回-1
        参数fd:发送者的文件描述符
        参数buf:数据缓冲区
        参数count:发送的数据大小

        9、接收数据 

// 接收数据
ssize_t recv(int sockfd, void *buf, size_t len, int flags);

// 接口说明
        返回值:成功返回实际接收的字节数,失败返回-1
            如果返回0,有以下几种情况:
            (1)、对等端已经有序关闭,下线了(TCP协议中经常出现)
            (2)、对等端发送一个字节数为0的数据报
            (3)、如果请求从流套接字接收的字节数为0,则也可能返回值0。

        参数sockfd:接收者套接字
        参数buf:数据缓冲区
        参数len:最大可接受的字节数
        参数flags:有很多种,一般设置为0

// 接收数据,也可以使用这个
ssize_t read(int fd, void *buf, size_t count);

// 接口说明
        返回值:成功返回实际接收的字节数,失败返回-1
            如果返回0,对等端已经有序关闭,下线了(TCP协议中经常出现)
        参数buf:数据缓冲区
        参数len:最大可接受的字节数
            

        10、关闭套接字

// 关闭套接字
int close(int fd);

// 接口说明
        返回值:成功返回0,失败返回-1
        参数fd:套接字文件描述符

六、案例

        使用TCP协议完成C/S架构的客户端和服务端通信演示

        客户端TcpClient.c

// TCP客户端的案例

#include 
#include 
#include 
#include 
#include        
#include 
#include 
#include 
#include 
#include 

#define CLIENT_IP   "192.168.64.128"    // 记得改为自己IP
#define CLIENT_PORT 10000   // 不能超过65535,也不要低于1000,防止端口误用

// 自定义的退出信号响应函数
void exit_handler(int sig)
{
    printf("[%d] exit\n", getpid());
    exit(0);
}

int main(int argc, char *argv[])
{
    // 注册自定义退出信号响应函数
    signal(34, exit_handler);

    // 1、建立套接字,指定IPV4网络地址,TCP协议
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1)
    {
        perror("socket fail");
        return -1;
    }

    // 2、设置端口复用(推荐)
    int optval = 1; // 这里设置为端口复用,所以随便写一个值
    int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    if(ret == -1)
    {
        perror("setsockopt fail");
        close(sockfd);
        return -1;
    }

    // 3、绑定自己的IP地址和端口号(可以省略)
    struct sockaddr_in client_addr = {0};
    socklen_t addr_len = sizeof(struct sockaddr);
    client_addr.sin_family = AF_INET;   // 指定协议为IPV4地址协议
    client_addr.sin_port = htons(CLIENT_PORT);  // 端口号
    client_addr.sin_addr.s_addr = inet_addr(CLIENT_IP); // IP地址
    
    ret = bind(sockfd, (struct sockaddr*)&client_addr, addr_len);
    if(ret == -1)
    {
        perror("bind fail");
        close(sockfd);
        return -1;
    }

    // 4、向服务器发起连接请求
    uint16_t port = 0;  // 端口号
    char ip[20] = {0};  // IP地址
    struct sockaddr_in server_addr = {0};
    printf("please input server IP and port:\n");
    scanf("%s %hd", ip, &port);
    printf("IP = %s, port = %hd\n", ip, port);

    server_addr.sin_family = AF_INET;   // 指定IPV4地址类型
    server_addr.sin_port = htons(port); // 服务器端口号
    server_addr.sin_addr.s_addr = inet_addr(ip);    // 服务器IP地址
    
    // 设置简单的超时重连
    for(int i = 0; i < 10; i++)
    {
        // 向服务器发起连接请求,第一次握手
        ret = connect(sockfd, (struct sockaddr*)&server_addr, addr_len);
        if(ret == -1)
        {
            perror("connect fail");
            printf("try to connect after 1 second\n");
            sleep(1);
            if(i == 10)
            {
                printf("server log out, please connect again later\n");
                close(sockfd);
                return -1;
            }
        }
        else
        {
            printf("connect server success\n");
            break;  // 成功连接就退出
        }
    }
    
    // 5、收发数据
    char msg[128] = {0};    // 数据缓冲区
    pid_t pid = fork();
    // 父进程负责发送数据
    if(pid > 0)
    {
        while(getchar() != '\n');   // 清空多余的换行符
        while(1)
        {
            printf("please input data:\n");
            fgets(msg, sizeof(msg)/sizeof(msg[0]), stdin);

            ret = send(sockfd, msg, strlen(msg), 0);
            if(ret > 0)
            {
                printf("success: send %d bytes\n", ret);
            }
            else
            {
                perror("send error");
            }
        }
    }
    // 子进程负责接收数据
    else if(pid == 0)
    {
        while(1)
        {
            ret = recv(sockfd, msg, sizeof(msg)/sizeof(msg[0]), 0);
            if(ret > 0)
            {
                printf("recv data: %s\n", msg);
                memset(msg, 0, sizeof(msg));
            }
            // 服务器已经掉线,这里直接退出
            else if(ret == 0)
            {
                printf("server log out\n");
                close(sockfd);
                kill(getppid(), 34);    // 给父进程发送退出信号
                kill(getpid(), 34);     // 给自己发送退出信号
            }
        }
    }
    else
    {
        perror("fork fail");
        close(sockfd);
        return -1;
    }

    return 0;
}

        服务端TcpServer.c

 

// TCP服务器的案例

#include 
#include 
#include 
#include 
#include 
#include        
#include 
#include 
#include 
#include 
#include 

#define MAX_LISTEN  50  // 最大能处理的连接数
#define SERVER_IP   "192.168.64.128"    // 记得改为自己IP
#define SERVER_PORT 20000   // 不能超过65535,也不要低于1000,防止端口误用


// 自定义的退出信号响应函数
void exit_handler(int sig)
{
    printf("[%d] exit\n", getpid());
    exit(0);
}


int main(int argc, char *argv[])
{
    // 注册自定义退出信号响应函数
    signal(34, exit_handler);

    // 1、建立套接字,指定IPV4网络地址,TCP协议
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd == -1)
    {
        perror("socket fail");
        return -1;
    }

    // 2、设置端口复用(推荐)
    int optval = 1; // 这里设置为端口复用,所以随便写一个值
    int ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
    if(ret == -1)
    {
        perror("setsockopt fail");
        close(sockfd);
        return -1;
    }

    // 3、绑定自己的IP地址和端口号(不可以省略)
    struct sockaddr_in client_addr = {0};
    socklen_t addr_len = sizeof(struct sockaddr);
    client_addr.sin_family = AF_INET;   // 指定协议为IPV4地址协议
    client_addr.sin_port = htons(SERVER_PORT);  // 端口号
    client_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // IP地址
    
    ret = bind(sockfd, (struct sockaddr*)&client_addr, addr_len);
    if(ret == -1)
    {
        perror("bind fail");
        close(sockfd);
        return -1;
    }

    // 4、设置监听
    ret = listen(sockfd, MAX_LISTEN);
    if(ret == -1)
    {
        perror("listen fail");
        close(sockfd);
        return -1;
    }

    // 5、接受连接请求
    printf("wait client connect...\n");
    uint16_t port = 0;
    char ip[20] = {0};
    int client_fd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
    if(client_fd == -1)
    {
        perror("accept fail");
        close(sockfd);
        return -1;
    }
    else
    {
        memset(ip, 0, sizeof(ip));
        strcpy(ip, inet_ntoa(client_addr.sin_addr));
        port = ntohs(client_addr.sin_port);
        printf("[%s:%d] connect\n", ip, port);
    }

    // 6、收发数据, 这里只做处理当个客户端的请求
    char msg[128] = {0};
    pid_t pid = fork();
    // 父进程负责发送数据
    if(pid > 0)
    {
        while(1)
        {
            printf("please input data:\n");
            fgets(msg, sizeof(msg)/sizeof(msg[0]), stdin);

            // 注意套接字是客户端的套接字,不是服务器的
            ret = send(client_fd, msg, strlen(msg), 0);
            if(ret > 0)
            {
                printf("success: send %d bytes\n", ret);
            }
            else
            {
                perror("send error");
            }
        }
    }
    // 子进程负责接收数据
    else if(pid == 0)
    {
        while(1)
        {
            ret = recv(client_fd, msg, sizeof(msg)/sizeof(msg[0]), 0);
            if(ret > 0)
            {
                printf("[%s:%d] send data: %s\n", ip, port, msg);
                memset(msg, 0, sizeof(msg));    // 清空数据区
            }
            // 客户端断开连接
            else if(ret == 0)
            {
                printf("[%s:%d] log out\n", ip, port);
                close(client_fd);
                close(sockfd);
                kill(getppid(), 34);    // 给父进程发送退出信号
                kill(getpid(), 34);     // 给自己发送退出信号
            }
        }
    }
    else
    {
        perror("fork fail");
        close(sockfd);
        return -1;
    }

    // 7、关闭套接字
    close(sockfd);

    return 0;
}

        通信演示

 Linux网络编程系列之TCP协议编程_第2张图片

Linux网络编程系列之TCP协议编程_第3张图片

Linux网络编程系列之TCP协议编程_第4张图片

        注:上述演示的服务器只能处理一个客户端连接,如果要处理多个可以看本系列中的服务器篇,在最开头有链接

七、总结

        TCP(Transmission Control Protocol)协议是一种面向连接的、可靠的、基于字节流的传输控制协议,属于传输层,任何需要可靠传输和连接保持的应用场景都可以使用TCP协议进行数据传输。TCP协议下的客户端通信流程和服务器通信流程不完全一致,可以结合案例加深对TCP协议的理解。

你可能感兴趣的:(Linux,C语言程序设计,c语言,linux)