【网络编程】网络编程套接字(三)TCP网络程序

news2024/11/25 0:41:53

文章目录

  • 简单的TCP网络程序
    • 一、服务器创建套接字
    • 二、服务器绑定套接字
    • 三、服务器监听
    • 四、服务器获取连接
    • 五、服务器处理请求
    • 六、对服务器进行简单测试
    • 七、客户端创建套接字
    • 八、客户端连接服务器
    • 九、客户端发起请求
    • 十、服务器客户端测试
  • 多进程的TCP服务器
    • 一、忽略SIGCHLD信号
    • 二、孙子进程提供服务
  • 多线程TCP服务器

简单的TCP网络程序

一、服务器创建套接字

与前边的UDP网络程序相同,创建套接字的接口都是socket,下边对socket接口进行介绍:
在这里插入图片描述
协议家族选择AF_INET,因为我们要进行网络通信。
而第二个参数,为服务类型,传入SOCK_STREAM,我们编写TCP程序,所以要选择流式的服务。
第三个参数默认传入0,由前两个参数就可以推出这是基于TCP的网络程序。

// 创建套接字
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            logMessage(FATAL, "create socket error %d-%s", errno, strerror(errno));
            exit(2);
        }

socket接口如果创建成功返回0,失败返回-1,并且错误码被设置,所以当返回值小于0时退出程序。

二、服务器绑定套接字

还是与UDP相同,绑定套接字需要bind接口,我们再次对bind接口进行学习:
在这里插入图片描述

第一个参数传入前边创建的套接字,也就是一个文件描述符。
第二个参数是一个sockaddr类型结构体的地址,内边存储着要绑定的IP和端口号的相关信息。
第三个参数为结构体的大小。

       // bind绑定套接字
        struct sockaddr_in local;
        // bzero((void*)&local,sizeof(local));
        memset(&local, 0, sizeof local);
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
       // local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        if (bind(_sock, (struct sockaddr *)&local, (socklen_t)sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind socket error %d-%s", errno, strerror(errno));
            exit(3);
        }

但是同时要注意网络序列和主机序列的转换,并且在处理IP地址时,也要注意到点分十进制与二进制的转换。

三、服务器监听

由于TCP协议是需要连接的,而UDP是不需要连接的,所以在对TCP的服务器进行创建,绑定套接字之后,必须进行监听操作,使服务器处于监听状态。这就例如:
一个商店老板,即使这会没有人来买东西,也必须坐在店里边,处于监听状态,一旦有人来买东西,就可以立马为客户服务。

一旦listen调用成功,服务器就会处于监听状态。

在这里插入图片描述
sockfd:需要设置为监听状态的套接字对应的文件描述符。
backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。
返回值:如果监听失败返回-1,并且错误码被设置,成功返回0.

// 监听
        if (listen(_sock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen socket error %d-%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init success,sockfd: %d", _sock);

当监听完成之后,服务器的初始化才算完成。

四、服务器获取连接

当服务器初始化完成之后,此时就要让客户端来连接,必须通过accept来获取连接,当客户端发送连接请求之后,服务器和客户端的连接才正式完成。
在这里插入图片描述
在这里插入图片描述
参数:
sockfd:监听套接字的文件描述符
addr:对端网络的相关信息结构体,例如IP,端口号,协议家族等
addrlen:addr结构体的大小
返回值:
accept的返回值有一些不同,如果返回成功,这些系统调用返回一个非负整数,它是一个描述符对于接受的套接字。如果出现错误,则返回-1,并适当地设置errno。

那么这个返回值是什么意思呢?为什么会有两个文件描述符,他们之间有什么关系?

当我们使用accpet进行连接时,是通过监听套接字进行连接的,但是当连接上对端网络之后,不是监听套接字来提供服务的,而是返回成功之后,会返回一个套接字的文件描述符,是由该服务套接字提供服务的。

  • 监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接。
  • accept函数返回的套接字:用于为本次accept获取到的连接提供服务。监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。
    下边通过一个例子来解释他们之间的关系:

当我们前往西安旅游时,一定想尝一尝正宗的羊肉泡馍,有一家店,服务员张三非常热情,一定在门口招呼路上的游客进去,当有一个游客准备进入餐馆吃饭时,张三就会喊一声,李四来人了,快出来招呼,但是张三又回到门口,继续让来往的游客进入餐馆,当下一个游客进入餐馆时,张三就说,王五来人了快来招呼人,此时服务顾客的人就是王五,而张三继续去外边找客人。

此处的张三我们就可以认为是监听套接字,主要功能就是不断的在外边找顾客,让顾客进入店内,也就是不断的获取新连接,而后边的李四王五赵六等等就相当于accept返回的服务套接字,他们才是为对端提供服务的。

// 建立连接
struct sockaddr_in src;
socklen_t len = sizeof(src);
int fd = accept(_sock, (struct sockaddr *)&src, &len);
if (fd < 0)
{
 	 logMessage(FATAL, "accept error %d-%s", errno, strerror(errno));
     continue;
}

五、服务器处理请求

通过以上的步骤,创建套接字,绑定套接字,监听,获取连接之后,当客户端对服务器进行连接之后,服务器就可以处理客户端发来的请求。

void service(int fd, const std::string &client_ip, const uint16_t &client_port)
{
    char buffer[1024];
    while (1)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout<<client_ip << ":" << client_port << "# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);
            break;
        }
        else
        {
            logMessage(FATAL, "read error %d-%s", errno, strerror(errno));
            break;
        }
        write(fd, buffer, strlen(buffer));
    }
    close(fd);
}

由于套接字在系统层面来看,就是打开的文件,所以对文件进行读写就可以使用我们之前学习过的 read和write接口。
read
读取数据时,使用read接口。

参数:
fd:文件描述符,表示从哪一个套接字中读取
buf:数据的存储位置,把数据读取到哪一个数组中
count:读取数据的大小
返回值:
当读取成功时,返回读取到的字节数,当写端关闭时,返回0,当读取错误时,返回小于0.

当返回值为0时,表示读取对端关闭了?

网络通信与进程间通信类似,和之前对文件读取写出相同:

  • 写端进程不写,读端进程一直读,此时读端进程就会被挂起,因为此时数据没有就绪。
  • 读端进程不读,写端进程一直写,此时当缓冲区被写满后写端进程就会被挂起,因为此时空间没有就绪。
  • 写端进程将数据写完后将写端关闭,此时当读端进程将管道当中的数据读完后就会读到0。
  • 读端进程将读端关闭,此时写端进程就会被操作系统杀掉,因为此时写端进程写入的数据不会被读取。

此处的情况就是写端也就是客户端将数据写完后将写端关闭,此时读端也就是服务器将数据读完之后就会读到0,所以返回值为0。


write
写入数据到网络时,需要使用write接口。
在这里插入图片描述
参数:
fd:写端套接字的文件描述符
buf:需要写入的数据
count:需要写入数据的字节数
返回值:
写入成功返回写入的字节数,写入失败返回-1,同时错误码被设置。


六、对服务器进行简单测试

当服务器初始化已经处理请求都完成之后,虽然还没有实现客户端,但是也可以telnet指令远程连接该服务器,实现请求处理服务:

第一步:
运行服务器,必须加上端口号,此时处于监听状态,等待客户端连接。
在这里插入图片描述
此时可以使用netstat指令观察该套接字的状态:
在这里插入图片描述
可以发现此时的服务器处于listen状态。

第二步:
使用telnet指令对服务器进行连接。

加粗样式

第三步:
对服务器进行请求。
在这里插入图片描述
在这里插入图片描述

七、客户端创建套接字

与前边创建套接字没有什么区别,注意的就是使用流式传输。
客户端是不需要绑定IP和端口号的,在客户端在连接时,系统会自动给客户端分配。
客户端也不需要监听,因为客户端不会被主动连接。

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);
    // 创建套接字
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
}

八、客户端连接服务器

客户端要发送请求时,必须要知道服务器的IP地址和端口号,所以我们使用命令行参数的方式,将服务器的Ip地址和端口号传给客户端,客户端接收之后之后,将IP地址和端口号传入addr结构体中,然后使用connect接口进行连接。
在这里插入图片描述

	struct sockaddr_in peer;
    memset(&peer,'\0', sizeof(peer));
    peer.sin_family = AF_INET;
    peer.sin_port = htons(server_port);
    peer.sin_addr.s_addr = inet_addr(server_ip.c_str());
    // 连接套接字
    if (connect(sock, (struct sockaddr *)&peer, (socklen_t)sizeof(peer)) < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(3);
    }
    std::cout << "connect success" << std::endl;

connect接口如果调用成功,客户端会被随机分配一下端口号,只要可以唯一标识客户端即可。

九、客户端发起请求

客户端与服务器连接成功之后,使用send接口发送数据,如果发送成功,返回值大于0。当发送成功之后,使用recv接口接收数据,最后在收到的数据后加上’\0’,将字符串回显。

    while (true)
    {
        std::string line;
        std::cout << "请输入# " << std::endl;
        getline(std::cin, line);
        if (line == "quit")
            break;
        ssize_t s = send(sock,line.c_str(),line.size(),0);
        if(s>0)
        {
            char buffer[1024];
            ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
            if(s > 0)
            {
                buffer[s]=0;
                std::cout<<"回显#"<<buffer<<std::endl;
            }
            else if(s==0)
            {
                break;
            }
            else
            {
                break;
            }
        }
    }

十、服务器客户端测试

服务端
在这里插入图片描述

客户端
在这里插入图片描述
当在客户端发起请求之后,客户端会与服务器建立连接,此时再次输入数据,服务器会对收到的数据进行回显。

多进程的TCP服务器

如果是单进程的服务器,当多个客户端同时启动,服务器只能处理一个客户端的请求,只有当第一个客户端退出之后,才会收到第二个客户端的请求。


为什么可以使用多进程
由于创建子进程后,子进程会继承父进程的文件描述符等信息,所以父进程创建的套接字也会被子进程继承下来,当我们使用多进程时,子进程就可以看到建立链接的文件描述符,并且当某一个进程处理完毕之后关闭文件描述符,也不会影响到其他的进程,因为父子进程具有独立性,在修改时会进行写时拷贝。


等待子进程问题
在子进程处理请求完毕之后,父进程必须等待子进程,要不然就会造成僵尸问题,会造成内存泄露,等待子进程有两种方式:

  1. 阻塞等待
  2. 非阻塞等待
    如果使用阻塞等待,那么说明父进程必须在等待第一个子进程服务完毕之后才可以处理下一个请求,本质上还是进行串行操作,并没有真正实现多进程。
    而如果使用非阻塞等待,虽然可以再进行其他的连接,但是必须不断的检测子进程是否退出。

为了解决以上的问题,我们可以采取两种方法:

  1. 对SIGCHLD进行自定义捕捉,主动忽略SIGCHLD信号,当子进程退出时,就会主动释放僵尸进程,父进程不会进行等待。
  2. 创建子进程,再让子进程创建子进程,让孙子进程进行服务,将子进程直接退出,当孙子进程处理完毕之后,成为孤儿进程被操作系统回收,所以父进程不需要进行等待。

一、忽略SIGCHLD信号

signal(SIGCHLD, SIG_IGN); // 对SIGCHLD,主动忽略SIGCHLD信号,子进程退出的时候,会自动释放自己的僵尸状态.

//version1.0多进程版,对信号进行忽略
            pid_t id = fork();
            assert(id != -1);
            if (id == 0)
            {
                close(_sock);
                service(fd, client_ip, client_port);
                exit(0);
            }
            close(fd);

二、孙子进程提供服务

先创建子进程,再让子进程创建子进程,让孙子进程提供服务,但是将子进程退出,当孙子进程提供完服务之后,被操作系统回收。

void service(int fd, const std::string &client_ip,
                    const uint16_t &client_port)
{
    char buffer[1024];
    while (1)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout<<client_ip << ":" << client_port << "# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);
            break;
        }
        else
        {
            logMessage(FATAL, "read error %d-%s", errno, strerror(errno));
            break;
        }
        write(fd, buffer, strlen(buffer));
    }
    close(fd);
}
//version1 .1多进程版,使用孙子进程进行服务 
pid_t id = fork();
if (id == 0)
{
    close(_sock);
    if (fork() > 0)
        exit(0);
    else
    {
        service(fd, client_ip, client_port);
        exit(0);
    }
}
waitpid(id, nullptr, 0);
close(fd);

多线程TCP服务器

服务器为了同时给多个客户端提供服务,不仅可以使用多进程来进行服务,也可以使用多线程来提供服务。
由于线程的回调函数中需要多个变量,所以我们将需要的IP,端口号,文件描述符写入一个类中,将实例化的对象指针传入回调函数,在回调函数中使用pthread_detach接口实现线程分离,主线程这边就不需要进行join回收线程了。

//服务函数
void service(int fd, const std::string &client_ip,
             const uint16_t &client_port)
{
    char buffer[1024];
    while (1)
    {
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;
        }
        else if (s == 0)
        {
            logMessage(ERROR, "%s-%d client close fd,me too!", client_ip.c_str(), client_port);
            break;
        }
        else
        {
            logMessage(FATAL, "read error %d-%s", errno, strerror(errno));
            break;
        }
        write(fd, buffer, strlen(buffer));
    }
    close(fd);
}

//线程数据
class pthreadData
{
public:
    int _sock;
    std::string _ip;
    uint16_t _port;
};

// version2多线程版
pthreadData *pd = new pthreadData();
pd->_port = client_port;
pd->_sock = fd;
pthread_t tid;
pthread_create(&tid, nullptr, Routine, pd);

//线程回调函数
static void *Routine(void *args)
{
    pthread_detach(pthread_self());
    pthreadData *pd = (pthreadData *)args;
    std::string client_ip = pd->_ip;
    uint16_t client_port = pd->_port;
    int sock = pd->_sock;
    service(sock, client_ip, client_port);
    return nullptr;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/765235.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IDEA 搭建Android 开发环境

项目实战 废话不多说开始创建先第一个 Android 项目 步骤一 FILE → New → Project 步骤二-选择 Android 项目模板 选那个安卓机器人,如果没有这个选项,需要升级IDEA版本或者安装安卓插件 选择*Basic Activity* Next-下一步 步骤三-项目初始化 名称、包名、安装位置自行调整…

守护数智未来,开源网安受邀参加2023OWASP北京论坛

2023年7月14日&#xff0c;OWASP中国与网安加社区联合举办的“2023OWASP中国北京安全技术论坛”在北京圆满召开&#xff0c;开源网安受邀参加本次论坛并分享“软件供应链安全治理实践”。 本次“2023OWASP中国北京安全技术论坛”是OWASP中国北京地区年度重要活动之一&#xff…

解决ValueError: If using all scalar values, you must pass an index

使用字典创建DataFrame对象时&#xff0c;会报这样得错误 ValueError: If using all scalar values, you must pass an index 代码如下&#xff1a; 这是因为没有索引所致&#xff0c;所以&#xff0c;需要我们指定索引&#xff0c;来创建DataFrame对象。 模式为&#xff1a…

【C++】模板进阶—非类型模板参数、模板特化及模板的分离编译

&#x1f680; 作者简介&#xff1a;一名在后端领域学习&#xff0c;并渴望能够学有所成的追梦人。 &#x1f681; 个人主页&#xff1a;不 良 &#x1f525; 系列专栏&#xff1a;&#x1f6f8;C &#x1f6f9;Linux &#x1f4d5; 学习格言&#xff1a;博观而约取&#xff0…

jar包发版至服务器Linux命令

maven -> clean compile install / package 使用WinSCP 或者其他软件在target下 找到jar包 传至服务器 找到原先jar包存放位置 cd /usr/local/xxx 通过线上端口号找到线程数 netstat -tunlp | grep 8080 杀死线程 kill -9 线程数 运行新jar包 nohup java -jar jar包名…

利用 jenkins 关联 Job 方式完善 RobotFramework 测试 Setup 以及 Teardown 后操作

目录 1.前言 2.Jekins 关联 Job 方式 1.前言 Jenkins是一个流行的持续集成和交付工具&#xff0c;它可以帮助自动化构建、测试和部署软件。与Robot Framework结合使用&#xff0c;可以实现更高效的测试工作流程。 在Robot Framework中&#xff0c;Setup和Teardown是测试用例…

文件IO_文件同步(附Linux-5.15.10内核源码分析)

目录 1.为什么要进行文件同步&#xff1f; 2.fsync函数介绍 2.1 fsync函数 2.2 fsync函数内核源码分析 2.3 fsync函数使用示例 3.fdatasync函数介绍 3.1 fdatasync函数 3.2 fdatasync函数内核源码分析 3.3 fdatasync函数使用示例 4.sync函数介绍 4.1 sync函数 4.2 …

第一阶段-第十一章 Python基础的综合案例(数据可视化-地图可视化)

目录 一、基础地图使用  1.学习目标  2.视觉映射器  3.本节的演示二、疫情地图-国内疫情地图  1.案例效果  2.函数的语法  3.本节的代码演示三、疫情地图-省级疫情地图  1.案例效果  2.本节的代码演示 说明&#xff1a;该文章是学习 黑马程序员在B站上分享的视…

SPRINGBOOT部署安装hello world

1.安装JAVA环境&#xff0c;设置为全局变量 用以下方法检查&#xff0c;安装是否正确 2.maven安装&#xff0c;并且修改setting中的镜像设置&#xff0c;改为国内阿里云镜像 3.idea中设置JDK版本号&#xff0c;IDEA中springboot不要选择3.0版本&#xff0c;会出现与jdk不匹…

如何在照片上添加水印?这三个方法让你轻松实现

我有个朋友他是一名摄影爱好者&#xff0c;他在旅行中经常能捕捉到一些绝美的照片。他为了分享这份美丽&#xff0c;决定将它们上传到社交媒体上。但是&#xff0c;他很担心别人未经许可就盗用了他的作品。于是他来想我请教这个问题。我就给他推荐了几款加水印软件&#xff0c;…

(学习笔记-TCP基础知识)TCP与UDP区别

UDP UDP不提供复杂的控制机制&#xff0c;利用IP提供面向[无连接]的通信服务。 UDP协议非常简单&#xff0c;头部只有8个字节(位)&#xff0c;UDP的头部格式如下&#xff1a; 目标和源端口&#xff1a;主要是告诉UDP协议应该把报文发给哪个进程包长度&#xff1a;该字段保存了…

CentOS目录详解

在centos中&#xff0c;最顶层的目录称作根目录&#xff0c; 用/表示。/目录下用户可以再创建目录&#xff0c;但是有一些目录随着系统创建就已经存在&#xff0c;接下来重点介绍几个常用目录。 /bin&#xff08;binary&#xff09;包含了许多所有用户都可以访问的可执行文件&a…

轻松实现金蝶云星空与赛意SMOM系统的全面集成

1. 金蝶云星空&#xff1a;为运营协同与管控型企业提供通用ERP服务平台 金蝶云星空是基于当今先进管理理论和数十万家国内客户最佳应用实践开发的ERP服务平台。它针对事业部制、多地点、多工厂等企业和集团公司&#xff0c;提供了通用的企业资源计划&#xff08;ERP&#xff0…

win键无效,键盘Win组合键突然不不能用如何解决?

电脑win键失效怎么办&#xff1f; 在使用windows系统的时候&#xff0c;发现一个问题&#xff0c;就是win键失效了&#xff0c;怎么按都没有反应&#xff0c;该怎么办呢&#xff1f; 键盘方面的原因 此时之是键盘的原因与系统本身没有关系&#xff0c;键盘屏蔽热键主要目标是…

【技能实训】DMS数据挖掘项目-Day13

文章目录 任务15【任务15.1】ClientFrame.java【任务15.2】ClientFrame.java【任务15.3】实现匹配日志信息或物流数据的数据保存功能&#xff08;保存到本地文件&#xff09;&#xff0c;将15.2中&#xff0c;返回的匹配数据&#xff0c;保存到客户端文件中【任务15.4】实现物流…

恢复软件哪些好?推荐3款,亲试好用!

“想问下朋友们有什么好的恢复软件推荐吗&#xff1f;我的电脑数据经常都莫名其妙就找不到了&#xff0c;我也不敢随意进行操作。如果有好的数据恢复软件&#xff0c;快给我推荐推荐吧&#xff01;” 电脑数据很多都是比较重要的&#xff0c;如果经常丢失数据&#xff0c;会对我…

DV SSL证书

一、DV SSL证书是什么&#xff1f; DV SSL&#xff08;又称域名验证型证书&#xff09;是便宜又快速实现网站HTTPS加密、有效防劫持的SSL证书。购买DV证书仅需百十元起&#xff0c;只需验证域名的所有权&#xff0c;3-5分钟极速签发。因此&#xff0c;DV SSL证书成为众多个人网…

探索新机遇,助力娱乐社交新增长丨网易云信亮相 PMTalk 北京产品运营大会

近日&#xff0c;在 PMTalk 联合网易易盾、网易云信主办的北京产品运营大会上&#xff0c;网易云信娱乐社交产品经理聂夏军介绍了 AIGC 与娱乐社交行业融合并助力业务新增长的思路&#xff0c;并分享了网易云信在帮助开发者业务增长方面的探索和实践经验。 由于国内娱乐社交市场…

nginx推流环境搭建

目录 1、创建安装文件夹2、安装编译 nginx 所需要的库3、下载 nginx-1.21.6.tar.gz下载 nginx-rtmp-module4、解压解压nginx文件解压rtmp模块5、编译6、安装7、启动nginx,检测nginx是否能成功运行8、配置nginx使用RTMP9、重启nginx服务器1、创建安装文件夹 cd ~ mkdir nginx …

无参数读文件和RCE总结

什么是无参数&#xff1f; 顾名思义&#xff0c;就是只使用函数&#xff0c;且函数不能带有参数&#xff0c;这里有种种限制&#xff1a;比如我们选择的函数必须能接受其括号内函数的返回值&#xff1b;使用的函数规定必须参数为空或者为一个参数等 接下来&#xff0c;从代码…