Socket 编程 TCP

目录

[1. TCP socket API 详解](#1. TCP socket API 详解)

[1.1 socket](#1.1 socket)

[1.2 bind](#1.2 bind)

[1.3 listen](#1.3 listen)

[1.4 accept](#1.4 accept)

[1.5 read&&write](#1.5 read&&write)

[1.6 connect](#1.6 connect)

[1.7 recv](#1.7 recv)

[1.8 send](#1.8 send)

[1.9 popen](#1.9 popen)

[1.10 fgets](#1.10 fgets)

[2. EchoServer](#2. EchoServer)

[3. 多线程远程命令执行](#3. 多线程远程命令执行)

[4. 引入线程池版本翻译](#4. 引入线程池版本翻译)

[5. 验证TCP - windows作为client访问Linux](#5. 验证TCP - windows作为client访问Linux)

[6. connect的断线重连](#6. connect的断线重连)


1. TCP socket API 详解

下面介绍程序中用到的socket API,这些函数都在sys/socket.h中。

1.1 socket

cpp 复制代码
// main socket
NAME
       socket - create an endpoint for communication

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

       int socket(int domain, int type, int protocol);
// domain:域,表示要进行网络通信,AF_INET
// type:套接字类型,TCP就填SOCK_STREM(流式套接字),UDP就填SOCK_DGRAM(数据包套接字)
// protocol:设置为0,默认就是TCP Socket

// 返回值
RETURN VALUE
       On success, a file descriptor for the new socket is returned.  On error, -1 is returned, and errno is set appropriately.
// 跟UDP一样,成功就是新的文件描述符,失败就返回-1

1.2 bind

cpp 复制代码
// man bind
NAME
       bind - bind a name to a socket

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

       int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);
// 填充当前服务器本地的socket信息,文件信息和网络信息关联起来。

// 返回值
RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.
// 如果成功了0被返回,否则-1被返回

1.3 listen

TCP的有连接和UDP的无连接

UDP叫面向数据报,无连接的,TCP叫面向字节流,有连接的,怎么证明UDP是无连接的呢?

只要我客户端一启动,随时就可以向服务器发消息,所以我们直接创建套接字之后,直接sendto就可以向服务器发消息了,没有所谓的连接,而TCP叫做面向连接的协议,即有连接,所以我们对应的TCP,要面向连接,如果我们的客户端(c)连接服务器(s),也叫cs模式,如果c向s发起对应的请求,就先的建立连接,连接建立成功才能通信,这就是TCP的特点,而客户点要连服务器,是不是要求这个服务器随时随地等待被连接。所以TCP是建立连接的,一定会存在协商的过程,所以tcp需要将socket设置为监听状态。

cpp 复制代码
// man listen
NAME
       listen - listen for connections on a socket
       // 意思就是再等别人随时随地来连接我

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

       int listen(int sockfd, int backlog);
// sockfd:设置的文件描述符
// backlog:服务器所对应的操作系统中所对应的全连接的个数,不建议为0,也不能太长,8,4,14,32都可以,具体设置多大,按照自己的需求来做,backlog是积压的意思,就类似于排队,但是排队也不能排太长

// 返回值
RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.
// 成功了返回0,失败了-1被返回

1.4 accept

cpp 复制代码
// man accept
NAME
       accept, accept4 - accept a connection on a socket

SYNOPSIS
       #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);

// 从指定的文件描述符当中获取新连接,获取新的客户端,当别人连接你的时候,第一件是就是要知道谁来连我,
// sockfd:套接字
// addr && addrlen:输出型参数,其实就相当于给我们传进来缓冲区,当我们再进行获取新连接,这个上面所对应的sockaddr的客户端的地址就会直接给我们返回。这两个参数就等同于recvfrom,recvfrom的后两个参数。

// 返回值
RETURN VALUE
       On success, these system calls return a nonnegative integer that is a file descriptor for the accepted socket.  On error, -1 is returned, errno is set appropriately, and addrlen is left unchanged.
// 如果成功就返回非0,失败-1返回。没有人连接就会阻塞。
// 成功后,这些系统调用将返回一个非负整数,该整数是所接受套接字的文件描述符,
// 正真提供服务的是由accept的返回值来定的。而sockfd是专门用来获取新连接的。

1.5 read&&write

读和写我们用的是read和write,因为我们的accept返回的是一个文件描述符,而且TCP是面向字节流的,管道和文件也是面向字节流的,所以TCP套接字,在我们获得了新的文件描述符之后,TCP套接字往后的处理都跟我们之前学习文件跟管道一样,我们可以调用read和write这样的文件接口来直接对网络进行读和写。

UDP不敢用read和write,因为UDP是面向数据报的,报文类型上是不一样的,至于什么是面向数据报,什么是面向字节流,单纯在应用层上编写代码是感受不到的,到TCP和UDP的原理时才能感受的到。

cpp 复制代码
void HandlerRequest(int sockfd) // TCP也是全双工
    {
        char inbuffer[4096];
        while(true)
        {
            size_t n = ::read(sockfd,inbuffer,sizeof(inbuffer)-1);
            if(n > 0)
            {
                LOG(LogLevel::INFO) << inbuffer;
                inbuffer[n] = 0;
                std::string echo_str = "server echo# ";
                echo_str += inbuffer;

                ::write(sockfd,echo_str.c_str(),echo_str.size());
            }
        }
    }
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            // 不能直接读数据
            // 1. 获取新连接
            struct sockaddr_in peer;
            socklen_t peerlen;
            int sockfd = ::accept(_listensockfd,CONV(&peer),&peerlen);
            if(sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);
                continue;// 失败了不是报错,而是回头重新来
            }

            // 获取连接成功了
            LOG(LogLevel::INFO) << "accept success,sockfd id : " << sockfd;

            // version-0
            HandlerRequest(sockfd);
        }
        _isrunning = false;
    }

1.6 connect

cpp 复制代码
// man connect
NAME
       connect - initiate a connection on a socket

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

       int connect(int sockfd, const struct sockaddr *addr,
                   socklen_t addrlen);
// sockfd:客户端所创建的套接字
// addr && addrlen:访问目标服务器所对应的IP地址和端口号,

// 返回值
RETURN VALUE
       If the connection or binding succeeds, zero is returned.  On error, -1 is returned, and  errno  is  set  appropri‐ately.
// 创建套接字成功,返回值就是0,否则就是-1.

1.7 recv

cpp 复制代码
// man recv
NAME
       recv, recvfrom, recvmsg - receive a message from a socket

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

       ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 从指定的文件描述符读取数据到buffer,长度是len,flage就是读取的标志位,默认设置为0就可以了,因为在我们当前的代码中,设置为0就表示的是阻塞的,他比read就多了一个flags,这是专门在套接字当中为我们处理IO接口,

1.8 send

cpp 复制代码
// man send
NAME
       send, sendto, sendmsg - send a message on a socket

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

       ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 把特定缓冲区指定长度通过该文件描述符发出去,他也多了一个flags参数。

不管是read、write、recv还是end,他们的返回值的意思都是一样的,所以可以平滑过渡。

1.9 popen

cpp 复制代码
// man popen
#include <stdio.h>
NAME
       popen, pclose - pipe stream to or from a process

SYNOPSIS
       #include <stdio.h>
       FILE *popen(const char *command, const char *type);
       int pclose(FILE *stream);
// 它的底层其实就是帮我们做了这几件事情
/*
1. 执行 Shell 命令
启动一个新的子进程,运行指定的命令行程序(如 ls, grep, python 等)。
2. 建立单向管道
根据模式(读或写),创建一个管道:
读模式("r"):从子进程的输出(如 stdout)读取数据。
写模式("w"):向子进程的输入(如 stdin)写入数据。
3. 返回文件流指针
返回一个 FILE* 指针,可通过标准文件操作函数(如 fread, fgets, fprintf)与子进程交互。
*/
// 1. pipe
// 2. fork + dup2(pipe[1],1) + exec*,执行结果给父进程,pipe[0]
// 3. return
// 返回值是FILE*的,其实就是把管道封装起来,封装成一个文件了。

// 返回值
RETURN VALUE
       popen(): on success, returns a pointer to an open stream that can be used to read or write to  the  pipe;  if  the fork(2) or pipe(2) calls fail, or if the function cannot allocate memory, NULL is returned.
       // 成功的话就返回一个文件流,失败返回空

       pclose(): on success, returns the exit status of the command; if wait4(2) returns an error, or some other error is detected, -1 is returned.

       Both functions set errno to an appropriate value in the case of an error.
       
// 一个线程执行fork就相当于当前进程创建子进程。

1.10 fgets

cpp 复制代码
// man fgets
NAME
       fgetc, fgets, getc, getchar, ungetc - input of characters and strings
SYNOPSIS
       #include <stdio.h>
       char *fgets(char *s, int size, FILE *stream);
// 返回值
RETURN VALUE
       fgets() returns s on success, and NULL on error or when end of file occurs while no characters have been read.
       // 读取失败返回NULL

2. EchoServer

Linux: This repository is specifically designed to store Linux code - Gitee.comhttps://gitee.com/Axurea/linux/tree/master/2025_05_26_EchoServer

3. 多线程远程命令执行

Linux: This repository is specifically designed to store Linux code - Gitee.comhttps://gitee.com/Axurea/linux/tree/master/2025_05_28_CommandServerTcp

4. 引入线程池版本翻译

  • 上篇UDP部分写过类似的设计方案。
  • 后面我们还会涉及http相关内容,到时候在引入线程池会更方便,也很合理。

5. 验证TCP - windows作为client访问Linux

Linux: This repository is specifically designed to store Linux code - Gitee.comhttps://gitee.com/Axurea/linux/tree/master/2025_05_28_Tcp_WindowsClient&&LinxuServer

注意:一定要开放云服务器对应的端口号,在你的阿里云或者腾讯云、华为云的网站后台中开放。

我们可以发现tcp client(windows)和tcp server(Linux)可以通信。

WinSock2.h是Windows Sockets API(应用程序接口)的头文件,用于在Windows平台上进行网络编程。它包含了Windows Sockets 2(WinSock2)所需的数据类型、函数声明和结构定义,使得开发者能够创建和使用套接字(sockets)进行网络通信。

在编写使用Winsock2的程序时,需要在源文件中包含WinSock2.h头文件。这样,编译器就能够识别并理解Winsock2中定义的数据类型和函数,从而能够正确的编译和链接网络相关的代码。

此外,与WinSock2.h头文件相对应的是ws2_32.lib库文件。在链接阶段,需要将这个库文件链接到程序中,以确保运行时能够找到并调用Winsock2 API中实现的函数。

在WinSock2.h中定义了一些重要的数据类型和函数,如:

WSADATA:保存初始化Winsock库时返回的信息。

SOCKET:表示一个套接字描述符,用于在网络中唯一标识一个套接字。

sockaddr_in:IPv4地址结构体,用于存储IP地址和端口号等信息。

socket():创建一个套接字。

bind():将套接字与本地地址绑定。

listen():将套接字设置为监听模式,等待客户端的连接请求。

accept():接受客户端的连接请求,并返回一个新的套接字描述符,用于与客户端

进行通信。

WSAStartup函数是Windows Sockets API的初始化函数,它用于初始化Winsock库。该函数在应用程序或DLL调用任何Windows套接字函数之前必须首先执行,它扮演着初始化的角色。

以下是WSAStartup函数的一些关键的:

它接受两个参数:wVersionRequested和IpWSAData。wVersionRequested用于指定所请求的Winsock版本,通常使用MAKEWORD(major,minor)宏,其中major和minor分别标识请求的主版本号和次版本号。IpWSAData是一个指向WSAData结构的指针用于接收初始化信息。

如果函数调用成功,它会返回0:否则,返回错误代码。

WSAStartup函数的主要作用是向操作系统说明我们将使用哪个版本的Winsock库,从而使得该库文件能与当前的操作系统协同工作。成功调用该函数后,Winsock库的状态会被初始化,应用程序就可以使用Winsock提供的一系列套接字服务,如地址家族识别、地址转换、名字查询和连接控制等。这些服务使得应用程序可以与底层的网络协议栈进行交互,实现网络通信。

在调用WSAStartup函数后,如果应用程序完成了对请求的Socket库的使用,应调用

WSAStartup函数后,如果应用程序完成了对请求的Socket库的使用,应调用WSACleanup

函数来解除与Socket库的绑定并释放所占用的系统资源。

6. connect的断线重连

客户端会面临服务器崩溃的情况,我们可以试着写一个客户端重连的代码,模拟并理解一些客户端行为,比如游戏客户端等。

采用状态机,实现一个简单的tcp client可以实现重连的效果。

Linux: This repository is specifically designed to store Linux code - Gitee.comhttps://gitee.com/Axurea/linux/tree/master/2025_05_28_TcpClientConnect

相关推荐
白总Server4 小时前
C++语法架构解说
java·网络·c++·网络协议·架构·golang·scala
I won.4 小时前
计算机网络 TCP篇常见面试题总结
tcp/ip·计算机网络·github
tiantianuser5 小时前
NVMe IP现状扫盲
服务器·网络·tcp/ip
不愧是你呀8 小时前
C++中单例模式详解
网络·c++·windows·单例模式
Rocky4019 小时前
HTTP和HTTPS
网络·网络协议·http
时之彼岸Φ10 小时前
网络攻防技术四:网络侦察技术
开发语言·网络
xiaohanbao0910 小时前
day44 python 训练CNN网络并使用Grad-CAM可视化
网络·人工智能·python·深度学习·学习·机器学习·cnn
巴拉特好队友10 小时前
捋捋wireshark
网络·测试工具·wireshark
iummature10 小时前
wireshark分析国标rtp ps流
网络·测试工具·wireshark
这儿有一堆花11 小时前
Wireshark 使用教程:让抓包不再神秘
网络·测试工具·wireshark