TCP网络编程

TCP网络编程

TCP介绍

1、面向连接的流式协议,可靠,出错重传,且每收到一个数据都要给出相应的确认
2、通信之前需要建立链接
3、服务器被动链接,客户端是主动链接

TCP与UDP差异

在这里插入图片描述
TCP和UDP流程对比图
在这里插入图片描述

TCP编程流程

  • 服务器
    • 创建套接字socket()
    • 将套接字与服务器网络信息结构体绑定bind()
    • 将套接字设置为监听状态listen()
    • 阻塞等待客户端的连接请求accept()
    • 进行通信recv()、send()
    • 关闭套接字close()
  • 客户端
    • 创建套接字socket()
    • 发送客户端连接请求connect()
    • 进行通信send()、recv()
    • 关闭套接字close()

TCP socket

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

在这里插入图片描述
在这里插入图片描述
案例

#include <stdio.h>   // printf
#include <sys/types.h>   //
#include <sys/socket.h>  // socket
#include <stdlib.h> // exit

int main(int argc, char *argv[])
{

    int sockfd;   // 文件描述符

    // 创建套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("fail to socket");
        exit(1);
    }

    printf("sockfd = %d\n", sockfd);

    return 0;
}

TCP客户端

connect函数

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

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

在这里插入图片描述

注意:

1、connect建立连接后不会产生新的套接字
2、连接成功后才可以开始传输TCP数据

send函数

#include <sys/types.h>
#include <sys/socket.h>

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

   ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                  const struct sockaddr *dest_addr, socklen_t addrlen);

   ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

在这里插入图片描述
注意:

不能用TCP协议发送0长度的数据包

recv函数

#include <sys/types.h>
#include <sys/socket.h>

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

   ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen);

   ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

在这里插入图片描述

客户端code

编写客户端代码,使用网络调试助手作为tcp服务端

#include <stdio.h>   // printf
#include <stdlib.h> // exit
#include<unistd.h>   // close
#include <sys/types.h>   //
#include <sys/socket.h>  // socket
#include<arpa/inet.h>   // htons  inet_addr
#include<netinet/in.h>
#include<string.h>
#include <sys/stat.h>

#define N 128

int main(int argc, char *argv[])
{
    if(argc < 3)
        {
            fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
            exit(1);
        }

    int sockfd;   // 文件描述符
    // 第一步:创建套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("fail to socket");
        exit(1);
    }

    // printf("sockfd = %d\n", sockfd);


    // 第二步:发送客户端连接请求
    struct sockaddr_in serveraddr;
    socklen_t addrlen = sizeof(serveraddr);

    serveraddr.sin_family = AF_INET;  // 协议族,AF_INET:ipv4网络协议

    // atoi函数是将字符串转换成整数
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);  // ip地址
    serveraddr.sin_port = htons(atoi(argv[2]));       // 端口号

    if(connect(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
    {
        perror("fail to connect");
        exit(1);
    }

    // 第三步:进行通信
    // 发送数据
    char buf[N] ="";
    // while (1)
    // {
        fgets(buf, N, stdin);
        buf[strlen(buf) - 1] = '\0';  // 把buf字符串中的\n转化为\0

        if(send(sockfd, buf, N, 0) == -1)
        {
            perror("fail to send");
            exit(1);
        }
    // }

    // 接收数据
    char text[N] = "";
    if(recv(sockfd, text, N, 0) == -1)
    {
        perror("fail to recv");
        exit(1);
    }

    // 打印接收到的数据
    printf("from server: %s\n", text);


    // 第四步:关闭套接字文件描述符
    close(sockfd);

    return 0;
}

在这里插入图片描述


TCP服务器-bind、listen、accept

对于面向连接的TCP协议来说,连接的建立才真正意味着数据通信的开始

bind函数

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

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

在这里插入图片描述
示例:
在这里插入图片描述

listen函数

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

   int listen(int sockfd, int backlog);

在这里插入图片描述

accept函数

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

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

   #define _GNU_SOURCE             /* See feature_test_macros(7) */
   #include <sys/socket.h>

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

在这里插入图片描述

TCP服务器例子

#include <stdio.h>   // printf
#include <stdlib.h> // exit
#include<unistd.h>   // close
#include <sys/types.h>   //
#include <sys/socket.h>  // socket
#include<arpa/inet.h>   // htons  inet_addr
#include<netinet/in.h>
#include<string.h>
#include <sys/stat.h>

#define N 128

int main(int argc, char *argv[])
{
    if(argc < 3)
        {
            fprintf(stderr, "Usage: %s <ip> <port>\n", argv[0]);
            exit(1);
        }

    // 第一步:创建套接字
    int sockfd;   // 文件描述符
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("fail to socket");
        exit(1);
    }

    // printf("sockfd = %d\n", sockfd);


    // 第二步:将套接字与服务器网络信息结构体绑定
    struct sockaddr_in serveraddr;
    socklen_t addrlen = sizeof(serveraddr);

    serveraddr.sin_family = AF_INET;  // 协议族,AF_INET:ipv4网络协议

    // atoi函数是将字符串转换成整数
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);  // ip地址
    serveraddr.sin_port = htons(atoi(argv[2]));       // 端口号

    if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) == -1)
    {
        perror("fail to bind");
        exit(1);
    }

    // 第三步:将套接字设置为被动监听状态
    if(listen(sockfd, 10) == -1)
    {
        perror("fail to listen");
        exit(1);
    }

    // 第四步:阻塞等待客户端的链接请求
    int acceptfd;
    struct sockaddr_in clientaddr;
    if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) == -1)
    {
        perror("fail to accept");
        exit(1);
    }

    // 打印连接的客户端信息
    printf("[%s - %d]\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

    // 第五步:进行通信
    // tcp服务器与客户端通信时,需要使用accept函数的返回值

    // 接收数据
    char text[N] = "";
    if(recv(acceptfd, text, N, 0) == -1)
    {
        perror("fail to recv");
        exit(1);
    }
    // 打印接收到的数据
    printf("from client: %s\n", text);


    // 发送数据
    strcat(text, " *_*");   // strcat(函数) 将两个char类型连接

    // while (1)
    // {
        if(send(acceptfd, text, N, 0) == -1)
        {
            perror("fail to send");
            exit(1);
        }
    // }


    // 关闭套接字文件描述符
    close(acceptfd);
    close(sockfd);

    return 0;
}

执行结果:

在这里插入图片描述

close关闭套接字

1、使用close函数即可关闭套接字
关闭一个代表已连接套接字将导致另一端收到一个0长度的数据包
2、做服务器时

  1. 关闭监听套接字将导致服务器无法接收新的连接,但不会影响已经建立的连接
  2. 关闭accept返回的已连接套接字将导致它所代表的连接被关闭,但不会影响服务器的监听

3、做客户端时

关闭连接就是关闭连接,不意味着其他

三次握手

在这里插入图片描述

四次挥手

在这里插入图片描述

TCP并发

注意: TCP不能实现并发的原因

由于TCP服务器端有两个读阻塞函数,accept和recv,两个函数需要先后运行,所以导致运行一个函数的时候,另一个函数无法执行,所以无法保证一边连接客户端,一边与其他客户端通信。

如何实现TCP并发服务?

使用多进程或多线程的方式实现

多进程并发案例

多进程并发案例

#include <stdio.h>   // printf
#include <stdlib.h> // exit
#include<unistd.h>   // close
#include <sys/types.h>   //
#include <sys/socket.h>  // socket
#include<arpa/inet.h>   // htons  inet_addr
#include<netinet/in.h>
#include<string.h>
#include <sys/wait.h>
#include <signal.h>
#include <signal.h>
#include <sys/stat.h>

// 使用多进程实现TCP并发服务器

#define N 128
#define ERR_LOG(errmsg) do{\
                            perror(errmsg);\
                            exit(1);\
                         }while(0)

void handler(int sig)
{
    wait(NULL);
}

int main(int argc, char *argv[])
{
    if(argc < 3)
        {
            fprintf(stderr, "Usage: %s <server_ip> <server_port>\n", argv[0]);
            exit(1);
        }

    int sockfd, acceptfd;  // 文件描述符
    struct sockaddr_in serveraddr, clientaddr;
    socklen_t addrlen = sizeof(serveraddr);

    // 第一步:创建套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        ERR_LOG("fail to socket");
    }

    // 将套接字设置为允许重复使用本地地址或者为设置为端口复用
    int on = 1;
    if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    {
        ERR_LOG("fail to setsockopt");
    }


    // 第二步:填充服务器网络信息结构体
    serveraddr.sin_family = AF_INET;  // 协议族,AF_INET:ipv4网络协议

    // atoi函数是将字符串转换成整数
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);  // ip地址
    serveraddr.sin_port = htons(atoi(argv[2]));       // 端口号

    // 第三步:将套接字与服务器网络信息结构体绑定
    if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
    {
        ERR_LOG("fail to bind");
    }

    // 第四步:将套接字设置为被动监听状态
    if(listen(sockfd, 5) < 0)
    {
        ERR_LOG("fail to listen");
    }

    // 使用信号,异步的方式处理僵尸进程
    signal(SIGCHLD, handler);

    while (1)
    {
    // 第五步:阻塞等待客户端的链接请求
    if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
    {
        ERR_LOG("fail to accept");
    }

    // 打印连接的客户端信息
    printf("[%s - %d]\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

    // 使用fork函数创建子进程,父进程继续负责连接,子进程负责与客户端通信
    pid_t pid;
    if((pid = fork()) < 0)
    {
        ERR_LOG("fail to fork");
    }
    else if(pid > 0)  // 父进程负责执行accept,所以if语句结束后继续在accept函数的位置阻塞
    {
    }
    else // 子进程负责跟指定的客户端通信
    {
        char buf[N] = "";
        ssize_t bytes;
        while (1)
        {
            if((bytes = recv(acceptfd, buf, N, 0)) < 0)
            {
                ERR_LOG("fail to recv");
            }
            else if(bytes == 0)
            {
                printf("The client quited\n");
                exit(0);
            }

            if(strncmp(buf, "quit", 4) == 0)
            {
                exit(0);
            }


            // 打印接收到的数据
            printf("from client: %s\n", buf);

            strcat(buf, " ^_^");  // strcat(函数) 将两个char类型连接
            if(send(acceptfd, buf, N, 0) < 0)
            {
                ERR_LOG("fail to send");
            }
        }
    }

    }

    return 0;
}

在这里插入图片描述


多线程并发案例

多线程并发案例

#include <stdio.h>   // printf
#include <stdlib.h> // exit
#include<unistd.h>   // close
#include <sys/types.h>   //
#include <sys/socket.h>  // socket
#include<arpa/inet.h>   // htons  inet_addr
#include<netinet/in.h>
#include<string.h>
#include <signal.h>
#include <sys/stat.h>
#include<pthread.h>

// 使用多线程实现TCP并发服务器

#define N 128
#define ERR_LOG(errmsg) do{\
                            perror(errmsg);\
                            exit(1);\
                         }while(0)

typedef struct{
    struct sockaddr_in addr;
    int acceptfd;
}MSG;

void *pthread_fun(void *arg)
{
    char buf[N] = "";
    ssize_t bytes;
    MSG msg = *(MSG *)arg;
    while (1)
    {
        if((bytes = recv(msg.acceptfd, buf, N, 0)) < 0)
        {
            ERR_LOG("fail to recv");
        }
        else if(bytes == 0)
        {
            printf("The client quited\n");
            pthread_exit(NULL);
        }

        if(strncmp(buf, "quit", 4) == 0)
        {
            printf("The client quited\n");
            pthread_exit(NULL);
        }

        // 打印接收到的数据
        printf("from client: %s\n", buf);
        printf("[%s - %d]\n", inet_ntoa(msg.addr.sin_addr), ntohs(msg.addr.sin_port));

        strcat(buf, " ^_^");  // strcat(函数) 将两个char类型连接
        if(send(msg.acceptfd, buf, N, 0) < 0)
        {
            ERR_LOG("fail to send");
        }
    }
}

int main(int argc, char *argv[])
{
    if(argc < 3)
        {
            fprintf(stderr, "Usage: %s <server_ip> <server_port>\n", argv[0]);
            exit(1);
        }

    int sockfd, acceptfd;  // 文件描述符
    struct sockaddr_in serveraddr, clientaddr;
    socklen_t addrlen = sizeof(serveraddr);

    // 第一步:创建套接字
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        ERR_LOG("fail to socket");
    }

    // 将套接字设置为允许重复使用本地地址或者为设置为端口复用
    int on = 1;
    if(setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    {
        ERR_LOG("fail to setsockopt");
    }


    // 第二步:填充服务器网络信息结构体
    serveraddr.sin_family = AF_INET;  // 协议族,AF_INET:ipv4网络协议

    // atoi函数是将字符串转换成整数
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);  // ip地址
    serveraddr.sin_port = htons(atoi(argv[2]));       // 端口号

    // 第三步:将套接字与服务器网络信息结构体绑定
    if(bind(sockfd, (struct sockaddr *)&serveraddr, addrlen) < 0)
    {
        ERR_LOG("fail to bind");
    }

    // 第四步:将套接字设置为被动监听状态
    if(listen(sockfd, 5) < 0)
    {
        ERR_LOG("fail to listen");
    }


    while (1)
    {
    // 第五步:阻塞等待客户端的链接请求
    if((acceptfd = accept(sockfd, (struct sockaddr *)&clientaddr, &addrlen)) < 0)
    {
        ERR_LOG("fail to accept");
    }

    // 打印连接的客户端信息
    // printf("[%s - %d]\n", inet_ntoa(clientaddr.sin_addr), ntohs(clientaddr.sin_port));

    // 创建子线程与客户端进行通信
    MSG msg;
    msg.addr = clientaddr;
    msg.acceptfd = acceptfd;
    pthread_t thread;
    if(pthread_create(&thread, NULL, pthread_fun, &msg)  != 0)
    {
        ERR_LOG("fail to pthread_create");
    }
    pthread_detach(thread);

    }

    return 0;
}

执行结果:

[root@iZ5bg01fils2m1fw0td6qgZ xhl]# gcc a_1.c -lpthread

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值