Socket编程——TCP协议

发布于:2025-08-04 ⋅ 阅读:(16) ⋅ 点赞:(0)

一、TCP传输

TCP和UDP类似,但是在传输中TCP有输入,输出缓冲区,看下面的传输图片

在这里插入图片描述
可以理解为TCP之间的数据传输都是依赖各自的socket,socket就充当传输的中介吧。

而每个socket都对应两个缓冲区,一个输入缓冲区,一个输出缓冲区 。

二、相关接口

下⾯介绍程序中⽤到的socketAPI,这些函数都在sys/socket.h中。

socket函数的声明:

int socket(int domain, int type, int protocol);

功能:打开网络文件(套接字)。

  • 参数domain:确定IP地址类型,如IPv4还是IPv6。
    AF_INET: IPv4。

    AF_INET6:IPv6。

  • 参数type:确定数据的传输方式。
    SOCK_STREAM: 流式套接字(TCP)。

    SOCK_DGRAM: 数据报套接字(UDP)。

  • 参数protocol:确定协议类型,如果前面type已经能确定了,这里传入0即可。

  • 返回值:
    成功:文件描述符。
    失败:一个小于0的数。

bind函数的使用

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

功能:用来绑定端口。

  • 参数sockfd:要绑定的套接字描述符(由 socket() 函数创建)。
  • 参数sockaddr:指向地址结构体的指针,包含绑定的IP地址和端口号。
  • 参数addrlen:地址结构体的长度(单位:字节)。
  • 返回值:
    0:成功。
    非0:失败

listen函数的使用

int listen(int sockfd, int backlog);

功能:将主动套接字(SOCK_STREAM)转换为被动监听状态,等待客户端连接请求。

  • 参数sockfd: 已绑定(bind)但未连接的套接字描述符
  • 参数backlog:等待连接队列的最大长度
    listen()声明sockfd处于监听状态,并且最多允许有backlog个客⼾端处于连接等待状态,如果接收
    到更多的连接请求就忽略
  • 返回值:
    0:成功。
    非0:失败

accept函数的使用

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

功能:三次握⼿完成后,服务器调⽤accept()接受连接;
如果服务器调⽤accept()时还没有客⼾端的连接请求,就阻塞等待直到有客⼾端连接上来

  • 参数sockfd:处于监听状态(LISTEN)的套接字描述符
  • 参数addr:addr是⼀个输出型参数,accept()返回时传出客⼾端的地址和端⼝号;
    如果给addr参数传NULL,表⽰不关⼼客⼾端的地址;
    参数addrlen:是⼀个传⼊传出参数(value-result argument),传⼊的是调⽤者提供的,缓冲区addr
    的⻓度以避免缓冲区溢出问题,传出的是客⼾端地址结构体的实际⻓度(有可能没有占满调⽤者提
    供的缓冲区);
  • 返回值
    -≥0 成功,返回新套接字描述符(与客户端通信用)
    -1 失败,错误码存储在errno中

connect函数的使用

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

功能:客⼾端需要调⽤connect()连接服务器;

  • connect和bind的参数形式⼀致,区别在于bind的参数是⾃⼰的地址,⽽connect的参数是对⽅的
    地址;

  • 成功返回0,出错返回-1;

下面就来根据缓冲区来说说,send()/write() 与 recv()/read() 两个函数吧。

注意: read(),write()是通用文件IO, recv(),send()是socket专用,在这里用哪一个都可以

send()/write()

ssize_t send(int sockfd, const void buf[.len], size_t len, int flags);
  • sockfd: socket套接字描述符

  • buf: 数据缓冲区

  • len: 要写入的字节数

  • flags: 控制标志

ssize_t write(int fd, const void *buf, size_t count);
  • fd: 任意文件描述符

  • buf: 数据缓冲区

  • count: 要写入的字节数

功能:都是用于数据发送或写入的函数

记住 ,我们只需要知道send()/write() 是将数据包发送到缓冲区里就行了,至于是什么时候将缓冲区里的数据发送到接收端的socket就不用我们应用层来管了,实际上想管也管不到。

有两种情况会将缓冲区里的数据全部发送出去

  • 情况1:缓冲区满了,很好理解吧,满了当然得发走啊,不然哪里有新的空间放新数据?

  • 情况2:隔到一定时间,发现没有数据再继续send到缓冲区里来了,即便没有满也会赶紧发过去,而这个时间是非常短的。

所以,不用担心说数据发送不及时,而为什么要这样,则是为了提高数据发送的效率了。

recv()/read()

ssize_t recv(int sockfd, void buf[.len], size_t len,  int flags);
  • sockfd: socket套接字描述符

  • buf: 数据缓冲区

  • len: 缓冲区可用容量(字节数)

  • flags: 控制标志

ssize_t read(int fd, void *buf, size_t count);
  • fd: 任意文件描述符

  • buf: 数据缓冲区

  • count: 期望读取的最大字节数

功能:都是用于接收或读取数据的函数

socket不论是发送还是接收缓冲区就相当于一个管道啊,先进先出,读取出来就不在管道里了。

简单的通信代码

client.cc

#include <iostream>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string>
#include <strings.h>
#include <arpa/inet.h>

int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        std::cout << "Usage " << argv[0] << " ip port" << std::endl;
        exit(1);
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("socket fail");
        exit(1);
    }

    uint16_t port = std::stoi(argv[2]);
    std::string ip = argv[1];

    sockaddr_in sd;
    bzero(&sd, sizeof(sd));

    sd.sin_family = AF_INET;
    sd.sin_port = htons(port);
    inet_pton(AF_INET, ip.c_str(), &sd.sin_addr);

    int n = connect(sock, (const sockaddr*)&sd, sizeof(sd));
    if(n < 0)
    {
        perror("connect fail");
        exit(1);
    }

    while(true)
    {
        std::string message;
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);

        ssize_t w = write(sock, message.c_str(), message.size());
        if(w <= 0)
        {
            perror("write fail");
            break;
        }

        char buffer[4096];

        ssize_t r = read(sock, buffer, sizeof(buffer));
        if(r < 0)
        {
            perror("read fail");
            break;
        }
        else if(r > 0)
        {
            buffer[r] = '\0';
            std::cout << "接收服务端: " << buffer << std::endl;
        }
        else
        {
            break;
        }
        
    }

    close(sock);
    return 0;
}

server.cc

#include <iostream>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string>
#include <strings.h>
#include <arpa/inet.h>

const int default_backlog = 4;

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        std::cout << "Usage "<< argv[0] << " port" << std::endl;
        exit(1);
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if(sock < 0)
    {
        perror("socket fail");
        exit(1);
    }

    int opt = 1;
    setsockopt(sock, SOL_SOCKET,SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

    uint16_t port = std::stoi(argv[1]);
    
    sockaddr_in sd;
    bzero(&sd, sizeof(sd));

    sd.sin_addr.s_addr = INADDR_ANY;
    sd.sin_family = AF_INET;
    sd.sin_port = htons(port);

    int b = bind(sock, (const sockaddr*)&sd, sizeof(sd));
    if(b < 0)
    {
        perror("bind fail");
        exit(1);
    }

    int list = listen(sock, default_backlog);
    if(list < 0)
    {
        perror("listen fail");
        exit(1);
    }

    while(true)
    {
        sockaddr_in sd;
        socklen_t len = sizeof(sd);

        int sockfd = accept(sock, (sockaddr*)&sd, &len);
        if(sockfd < 0)
        {
            std::cout << "accept fail" << std::endl;
            continue;
        }

        std::cout << "接收成功 ip: " << inet_ntoa(sd.sin_addr) << " port: " << ntohs(sd.sin_port) << std::endl;
        
        while(true)
        {
            char buffer[1024];
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if(n < 0)
            {
                perror("read fail");
                break;
            }
            else if(n == 0)
            {
                std::cout << "client quit" << std::endl;
                break;
            }
            else
            {
                buffer[n] = '\0';
                std::cout << "client say: " << buffer << std::endl;

                std::string echo = "server say: ";
                echo += buffer;

                ssize_t w = write(sockfd, echo.c_str(), echo.size());
                if(w <= 0)
                    exit(1);
                
            }

        }
        
        close(sockfd);
        
    }
    
    close(sock);

    return 0;
}

测试多个连接的情况
再启动⼀个客⼾端,尝试连接服务器,发现第⼆个客⼾端,不能正确的和服务器进⾏通信

分析原因,是因为我们 accecpt 了⼀个请求之后,就在⼀直 while 循环尝试 read ,没有继续调⽤
到 accecpt ,导致不能接受新的请求

我们当前的这个 TCP ,只能处理⼀个连接,这是不科学的,我们需要引入多进程或者多线程

三、多进程版本

#include <iostream>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string>
#include <strings.h>
#include <arpa/inet.h>

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

const int default_backlog = 4;

void Handler(int sockfd, const sockaddr_in& sd) 
{
    std::cout << "接收成功 ip: " << inet_ntoa(sd.sin_addr) << " port: " << ntohs(sd.sin_port) << std::endl;

    while (true)
    {
        char buffer[1024];
        ssize_t n = read(sockfd, buffer, sizeof(buffer));
        if (n < 0)
        {
            perror("read fail");
            break;
        }
        else if (n == 0)
        {
            std::cout << "client quit" << std::endl;
            break;
        }
        else
        {
            buffer[n] = '\0';
            std::cout << "client say: " << buffer << std::endl;

            std::string echo = "server say: ";
            echo += buffer;

            ssize_t w = write(sockfd, echo.c_str(), echo.size());
            if (w <= 0)
                break;
        }
    }
    close(sockfd);
    exit(0);
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Usage " << argv[0] << " port" << std::endl;
        exit(1);
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        perror("socket fail");
        exit(1);
    }

    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

    uint16_t port = std::stoi(argv[1]);

    sockaddr_in sd;
    bzero(&sd, sizeof(sd));

    sd.sin_addr.s_addr = INADDR_ANY;
    sd.sin_family = AF_INET;
    sd.sin_port = htons(port);

    int b = bind(sock, (const sockaddr *)&sd, sizeof(sd));
    if (b < 0)
    {
        perror("bind fail");
        exit(1);
    }

    int list = listen(sock, default_backlog);
    if (list < 0)
    {
        perror("listen fail");
        exit(1);
    }

    while (true)
    {
        sockaddr_in sd;
        socklen_t len = sizeof(sd);

        int sockfd = accept(sock, (sockaddr *)&sd, &len);
        if (sockfd < 0)
        {
            std::cout << "accept fail" << std::endl;
            continue;
        }

        pid_t id = fork();
        if(id < 0)
        {
            close(sockfd);
            continue;
        }
        else if(id == 0)
        {
            if(fork() > 0)
            {
                close(sockfd);
                exit(0);
            }
            
            Handler(sockfd, sd);
        }
        else
        {
            close(sockfd);

            waitpid(id, nullptr, 0);
        }

    }


    close(sock);

    return 0;
}

四、多线程版本

#include <iostream>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string>
#include <strings.h>
#include <arpa/inet.h>

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

#include <pthread.h>

const int default_backlog = 4;

struct Arg
{
    Arg(int sock, sockaddr_in s)
        : sockfd(sock), sd(s)
        {}

    int sockfd;
    sockaddr_in sd;
};

void* Handler(void* arg) 
{
    Arg* a = static_cast<Arg*>(arg);
    int sockfd = a->sockfd;
    sockaddr_in sd = a->sd;

    delete a;

    std::cout << "接收成功 ip: " << inet_ntoa(sd.sin_addr) << " port: " << ntohs(sd.sin_port) << std::endl;

    while (true)
    {
        char buffer[1024];
        ssize_t n = read(sockfd, buffer, sizeof(buffer));
        if (n < 0)
        {
            perror("read fail");
            break;
        }
        else if (n == 0)
        {
            std::cout << "client quit" << std::endl;
            break;
        }
        else
        {
            buffer[n] = '\0';
            std::cout << "client say: " << buffer << std::endl;

            std::string echo = "server say: ";
            echo += buffer;

            ssize_t w = write(sockfd, echo.c_str(), echo.size());
            if (w <= 0)
                break;
        }
    }
    close(sockfd);
    return nullptr;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cout << "Usage " << argv[0] << " port" << std::endl;
        exit(1);
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        perror("socket fail");
        exit(1);
    }

    int opt = 1;
    setsockopt(sock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

    uint16_t port = std::stoi(argv[1]);

    sockaddr_in sd;
    bzero(&sd, sizeof(sd));

    sd.sin_addr.s_addr = INADDR_ANY;
    sd.sin_family = AF_INET;
    sd.sin_port = htons(port);

    int b = bind(sock, (const sockaddr *)&sd, sizeof(sd));
    if (b < 0)
    {
        perror("bind fail");
        exit(1);
    }

    int list = listen(sock, default_backlog);
    if (list < 0)
    {
        perror("listen fail");
        exit(1);
    }

    while (true)
    {
        sockaddr_in sd;
        socklen_t len = sizeof(sd);

        int sockfd = accept(sock, (sockaddr *)&sd, &len);
        if (sockfd < 0)
        {
            std::cout << "accept fail" << std::endl;
            continue;
        }

        Arg* a = new Arg(sockfd, sd);

        pthread_t id;
        int n = pthread_create(&id, nullptr, Handler, (void*)a);
        if(n < 0)
        {
            close(sockfd);
            delete a;
            continue;
        }
        
        pthread_detach(id);

    }


    close(sock);

    return 0;
}

网站公告

今日签到

点亮在社区的每一天
去签到