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

文章目录

  • 简单的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.hqwc.cn/news/27507.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

django报错设置auth User

1.报错&#xff1a;auth.User.groups... auth.User.user_permissions... 我们的用户组、用户权限只能关联一个用户 &#xff0c;我们自己定义了一个用户表&#xff0c;系统还有一个用户表&#xff0c;这时候就会出问题。 解决办法&#xff1a; 让给我们自己定义的user替换系…

以高质量产业载体为底色,绘就珠海高新区产业发展新图景

【作者】珠海高新招商 “珠海高新招商”以招商运营为核心&#xff0c;聚焦珠海工业园区、珠海5.0产业园等招商引资工作&#xff0c;依托专业的招商团队和丰富的创新资源&#xff0c;为企业提供产业园入驻、平台搭建、产业政策咨询、科技服务等全流程专业服务。推动高新区招商引…

Android平台如何高效率实现GB28181对接?

技术背景 GB28181协议是一种用于设备状态信息报送的协议&#xff0c;可以在不同设备之间进行通信和数据传输。 在安卓系统上实现GB/T 28181非常必要&#xff0c;GB28181协议实现分两部分&#xff0c;一部分是信令&#xff0c;另外一部分就是媒体数据的编码。 信令主要包括S…

MP4格式视频怎么转mov格式?好用的视频格式转换方法分享

MOV格式是苹果公司的专有格式&#xff0c;因此在苹果设备上播放MOV格式的视频时&#xff0c;兼容性更好&#xff0c;因此可以实现更高质量的视频。如果我们需要高质量的视频输出&#xff0c;将MP4转换为MOV格式可能是个好选择。那么怎么进行转换呢&#xff1f;给大家分享几种简…

解决Hadoop集群hive库建表中文和表数据乱码问题

最近在测试环境,发现DDL建表后,发现中文注释和表数据乱码的问题,如下 查询元数据 原因是hive 的 metastore 支持的字符集是 latin1,所以中文写入的时候会有编码问题。 解决方案如下: 对MySQL的编码设置 [client]下面增加 default-character-set=utf8 在[mysqld]下面增…

12_基于 I2C 协议的 EEPROM 驱动控制

12_基于 I2C 协议的 EEPROM 驱动控制 1. I2C协议1.1 I2C通信协议1.2 I2C物理层1.3 I2C协议层1.3.1 单字节数据的写入1.3.2 页写数据写入1.3.3 随机读取操作1.3.4 顺序读取操作 2. EEPROM2.1 板载 EEPROM 实物图2.2 板载 EEPROM 部分原理图 3. 实验目标4. 模块框图4.1 顶层模块4…

线程池学习(二)execute() 和 submit() 的区别

转载&#xff1a;线程池 线程提交的两种方式 ExecutorService poll3 Executors.newCachedThreadPool();for (int i 0; i < 2; i) {poll3.execute(new TargetTask());poll3.submit(new TargetTask());}execute方法 void execute(Runnable command): Executor接口中的方法s…

【从零开始学习CSS | 第三篇】选择器优先级

目录 前言&#xff1a; 常见选择器的优先级&#xff08;从高到低&#xff09; 选择器的权重&#xff1a; 总结&#xff1a; 前言&#xff1a; 在前几篇文章中我们介绍了大量的选择器&#xff0c;那么大量的选择器在使用的时候&#xff0c;一定是有一个优先级顺序的&#xff…

2快速入门Spring基于XML的方式注册第一个组件

基于XML的方式注册第一个组件 开发步骤 第一步&#xff1a;创建Maven工程配置生成的pom.xml文件, 添加spring context基础依赖和junit依赖(注意根据Spring官方文档描述,Spring6需要JDK版本17) 当添加Spring的基础依赖spring context之后&#xff0c;Maven会自动关联并引入其…

【golang中的切片的相关知识点】[ ] slice

golang-切片 切片的定义和初始化切片的内存分析切片的操作获取长度和容量追加元素复制切片 切片的遍历切片的特性总结 Golang中的切片是一种灵活且强大的数据结构&#xff0c;它可以动态地增长和缩小。切片是基于数组的抽象&#xff0c;它提供了更方便的操作和更灵活的内存管理…

什么是分布式微服务?

什么是分布式微服务&#xff1f; 前言什么是微服务举例说明 什么是分布式图解分布式与微服务单体架构及部署微服务架构分布式影响 分布式微服务架构什么是分布式微服务架构面临的问题 前言 本文旨在讲清楚什么是分布式微服务架构&#xff0c;通过解释微服务架构和分布式架构&a…

瀚高数据库企业版V4单机版-安装手册(Windows)

目录 瀚高数据库企业版V4单机版-安装手册&#xff08;Windows&#xff09; 1. 环境准备 2. 软件安装 3.设置环境变量 4 配置数据库文件 瀚高数据库企业版V4单机版-安装手册&#xff08;Windows&#xff09; 1. 环境准备 ①.安装数据库之前&#xff0c;请确保vcredist_x6…