qt socket 传递结构体 结构体中有list_Socket 笔记

套接字是计算机进程用来交换数据的通信点,它允许两个进程,通过标准 Unix 文件描述符,进行通信,是应用层对传输层的编程接口,位于应用层和传输层之间,通过端口(port)识别进程,通过IP地址识别主机。

e637c4a5fd71c7c4f243230d2357b154.png

在 Unix 系统中,所有的 I/O 行为,都是通过读写文件描述符(一个与打开文件关联起来的整数,如网络连接、文本文件、终端等)完成。用于客户端与服务器之间的通信。

a9c7f79342ec91f90eeaded00029e7ea.png
双向且面向进程的通信

共有四种类型的 Socket,前两种常用。通常假定进程是在同一类型的套接字之间通信,但不同类型套接字之间亦可通信的限制。

  • Stream Sockets − 使用 TCP 协议传输数据,保证收发顺序一致,如发送 "A, B, C",接收端必然会以 "A, B, C" 的顺序接收。如果发送失败,发送端会收到一个错误提示。数据包之间没有边界,需要应用层自己处理。
  • Datagram Sockets − 使用 UDP 协议传输数据,通信的两个进程在发送数据前不需要建立起连接,只需要知道目的进程便可创建包并发送数据。
  • Raw Sockets原始套接字,用于访问底层的支持“套接字抽象”的通信协议,通常是面向数据报的。用于接收本机网卡上的数据帧或者数据包、监听网络的流量和分析、开发新的通信协议。
  • Sequenced Packet Sockets有序数据包套接字,与 stream socket 相似-具有包边界。它是 Network Systems (NS) 套接字抽象的一部分。允许用户通过它操作与接收包中 Sequence Packet Protocol (SPP) 或 Internet Datagram Protocol (IDP) 的头部。!!

网络地址-IP (host) Address

IP (host) Address 用来识别主机,IP 即 Internet Protocol ,是指因特网的整体网络架构的网络层(Internet Layer), IP = network . host ,共32 bit 或是 64 bit 。

32 为 IP 地址分类如下:

  • Class A addresses begin with 0xxx, or 1 to 126 decimal.
  • Class B addresses begin with 10xx, or 128 to 191 decimal.
  • Class C addresses begin with 110x, or 192 to 223 decimal.
  • Class D addresses begin with 1110, or 224 to 239 decimal 多播地址
  • Class E addresses begin with 1111, or 240 to 254 decimal 预留地址

Addresses beginning with 127 decimal, are reserved for loopback and for internal testing on a local machine [You can test this: you should always be able to ping 127.0.0.1, which points to yourself];

可以通过对 host 进一步划分产生子网,控制流量或是适应硬件异构,即host = subnet . subhost 。

客户端服务器模型

大多少网络应用都采用客户服务器模型。

ab5f2bad4a0e757233ab2baccca565c1.png
2-tier 架构

2-tier 与 3-tier 架构

There are two types of client-server architectures −

  • 2-tier 架构 − 客户端直接与服务端通信,该模型通常会有安全漏洞与性能问题,可以通过 Secure Socket Layer (SSL)解决安全问题。
  • 3-tier 架构 − 客户端与服务端直接,会插入一个或是多个软件-中间件,用来进行安全检验,做负载均衡。

961d5b0a286936b3a964189fb5e369b1.png
3-tier 架构

Stream Sockets-创建 socket 步骤

客户端创建 socket 步骤:

  • Create socket()
  • Connect connect()
  • Send & receive data. 有多种实现方式,最简单是使用 read() and write()

服务端创建 socket 步骤:

  • Create socket()
  • Bind socket to address bind() 因特网地址 = port number + host
  • Listen connections listen()
  • Accept connection accept() ,blocks connection until a client connects with the server
  • Send and receive data with read() and write()

e9c64b444810308a7e18232235a2bf7a.png
Stream Sockets-客户端与服务端交互流程

Socket 结构体

struct sockaddr {
   unsigned short   sa_family;
   char             sa_data[14];//sockaddr_in
};

sa_family,地址家族,通常使用 AF_INET,{ AF_INET、AF_UNIX、AF_NS、AF_IMPLINK }

sa_data,根据 sa_family 解析,对于 AF_INET 协议家族,port number 和 IP address 存储在其中,即 sockaddr_in 结构体

struct sockaddr_in {
   short int            sin_family; //同sa_family
   unsigned short int   sin_port;  //服务端口,16位的端口号,Network Byte Order
   struct in_addr       sin_addr;  //IP地址,32位IP地址,Network Byte Order
   unsigned char        sin_zero[8];//NULL 没有使用
};

struct in_addr {
   unsigned long s_addr;//32 bit netid/hostid    Network Byte Order
};

主机信息结构

struct hostent {
   char *h_name; 
   char **h_aliases; 
   int h_addrtype;  
   int h_length;    
   char **h_addr_list
	
#define h_addr  h_addr_list[0]
};

服务端信息结构

struct servent {
   char  *s_name; 
   char  **s_aliases; 
   int   s_port;  
   char  *s_proto;
};

结构使用原则

  1. 使用引用传递结构,用另外一个参数传递结构体的长度。
  2. 如果一个函数填充结构体,使用引用传递结构体长度,即所谓的 value-result 参数。
  3. 使用bzero() 前必须使用 memset() 把结构体设置为 NULL。

端口与服务

使用 32-bit IP 地址识别机器,TCP 和 UDP 使用知名的端口(port)来识别进程。

客户端连接到服务端之前,必须有识别服务端的方法才可以。知道了IP地址就可以连接服务端了,但是如何识别某个进程呢?通信是进程间的通信,只识别主机进程是无法进行通信的。

2d0116c7b6c40c8d5973bd0fd288de75.png

为了解决识别通信的进程,TCP 和 UDP 定义了一组公认的端口(ports),即所有小于1024的端口,自定义端口号的阈为 [1024, 65535]。各种进程的端口号配置可在 /etc/services 查询。

If you are writing your own server then care must be taken to assign a port to your server. You should make sure that this port should not be assigned to any other server. 用尽了咋办?Normally it is a practice to assign any port number more than 5000.

端口分配与关联的服务:https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml

端口与服务函数

Unix 提供的从/etc/services 获取服务名和端口号的函数:

  • struct servent *getservbyname(char *name, char *proto) − map service name and protocol name to port number
  • struct servent *getservbyport(int port, char *proto) − map port number and protocol name to service name.

Socket 网络字节序(与大端一致)

不同的计算机,可能以不同的顺序,存储多字节值。 如一个两个字节,即16比特的因特网,由两种方式存储其值:

  • Little Endian 小端 − 把表意能力差的字节,放在低地址(地址A),表意能力强的字节,方在高地址(地址A+1)。
  • Big Endian 大端 - 与小端相反。

在建立因特网 socket 连接的过程中,必须保证结构体 sockaddr_in 的成员 sin_port 和 sin_addr ,以网络字节序表象。

下面是进程主机内部的数据表象形式和网络字节序数据表象形式转换历程:

  • unsigned short htons(unsigned short host short) − (2-byte) host byte order to network byte order.
  • unsigned long htonl(unsigned long host long) − (4-byte) quantities from host byte order to network byte order.
  • unsigned short ntohs(unsigned short net short) − 16-bit (2-byte) quantities from network byte order to host byte order.
  • unsigned long ntohl(unsigned long net long) − 32-bit quantities from network byte order to host byte order.

Socket - IP 地址函数

Unix 提供多种操作 IP 地址的函数,即 IP 地址函数,把因特网地址在 ASCII 字符串(人类易读)和网络字节序的二进制值(存储在 socket 地址结构中的值)之间相互转换。

IPv4 addressing

int inet_aton(const char *strptr, struct in_addr *addrptr)

It returns 1 if the string was valid and 0 on error.

#include <arpa/inet.h>

(...)

   int retval;
   struct in_addr addrptr  //Network Byte Order (bytes ordered from left to right)
   
   memset(&addrptr, '0', sizeof(addrptr));
   retval = inet_aton("68.178.157.132", &addrptr);

(...)

in_addr_t inet_addr(const char *strptr)

This function call converts the specified string in the Internet standard dot notation to an integer value suitable for use as an Internet address. The converted address will be in Network Byte Order (bytes ordered from left to right). It returns a 32-bit binary network byte ordered IPv4 address and INADDR_NONE on error.

Following is the usage example −

#include <arpa/inet.h>

(...)

   struct sockaddr_in dest;

   memset(&dest, '0', sizeof(dest));
   dest.sin_addr.s_addr = inet_addr("68.178.157.132");
   
(...)

char *inet_ntoa(struct in_addr inaddr)

This function call converts the specified Internet host address to a string in the Internet standard dot notation.

Following is the usage example −

#include <arpa/inet.h>

(...)

   char *ip;
   
   ip = inet_ntoa(dest.sin_addr);
   
   printf("IP Address is: %sn",ip);
   
(...)

Socket 核心流程

e9c64b444810308a7e18232235a2bf7a.png
  • int socket (int family, int type, int protocol); 双端创建socket
  • int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); 客户端发起连接
  • int bind(int sockfd, struct sockaddr *my_addr,int addrlen); 服务端绑定协议地址(IP + port)
  • int listen(int sockfd,int backlog); 服务端,把未连接的 socket 设置为被动 socket,意味着内核可以把新来的请求并交给它处理;内核入队的该 socket 的最大连接数为 backlog;
  • int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen); 返回连接队列中的下成功连接的文件描述符

高级概念

socket 有三种请求-服务模型:多线程、select() 和 poll():

使用多线程,一个请求一个线程为其服务。

select() 使用一个 bit 数组,来标识从 0 到 n-1 连续的 n个描述符是否需要读写。

poll() 使用离散的 pollfd 集合来标识一个个的描述符,不像 bit 数组,这里是按需分配 pollfd 结构体,不会出现浪费。

资源消耗的复杂行---逻辑操作的复杂行---面向对象的封装 poll

异步 I/O(overlapped I/O)

基于线程的客户端/服务器模型,可以使用异步 I/O 完成高并发且高效内存使用的 I/O。

  • 一个客户端连接一个线程,会消耗太多线程,引入大量的 sleep 和 wake-up 消耗。
  • 使用 select() ,多个客户端连接复用一个线程,弊端是引入了大量的冗余工作-选择或是标识每一个后续的 select ,逻辑复杂化。

在控制权交给用户应用后,异步 I/O 通过用户空间 buffers 的写入或读出,克服了上面两种方案的缺点。在数据可读时,或是连接可写入数据时,异步 I/O 通知这些工作线程。

阻塞 I/O

在阻塞 I/O 中,客户端到服务端的一个连接请求,会占用一个服务端线程。直到读线程读取完整的数据,放入缓冲中后才通知工作线程开始处理数据,在这个很长的等待期内,读线程空闲。select/poll 也是是阻塞 I/O 。

非阻塞 I/O

异步非阻塞 I/O 快速但是消耗CPU,是真正额异步 I/O 。

Select/poll 是异步阻塞的,扩展性差。

使用 fcntl() 打开 O_NONBLOCK ,或是使用 ioctl() 打开 FIONBIO ,立即返回不阻塞(默认阻塞),如果没有达到目的可以使用 poll() 或是 select()。

cntl是计算机中的一种函数,通过fcntl可以改变已打开的文件性质。

信号

应用可以向系统注册事件回调,事件发生后,使用 poll() 或是 select() 查看为什么接收到了信号。

IP 多播

应用发送一个 IP 数据报,网络中的一个组内的多个主机可以收到该 IP 数据,主机可在不同的网络中,可以随时向组内添加或是移除主机。

带外数据 Out-of-band Data

带内数据是正常的有序流数据,带外数据独立于带内数据。带外数据是一个概念上独立通道,其数据传输不需要保持顺序,是传输协议和数据通道固有的特性。该概念可能来源于通信行业的带外信号,即 out-of-band signaling 。

I/O 多播 - select()

select() 用于检哪个文件描述符可读、可写或是发生了错误。建议使用异步 I/O ,避免 select() ,以最大限度的使用应用的资源。

同异步 I/O ,select() 创建一个共用点,同时等待多个条件,它允许应用指定一组描述符,监测一下条件是否存在:

  • 有数据可读
  • 可写数据
  • 发生异常

fa75eae7fba5198c758467331240cc27.png
使用 select/poll 核心流程

小结

一共有五种 I/O 模型,即默认的 Blocking 模型、通过 fcntl() 设置 O_NONBLOCK 的 Nonblocking 模型、使用 select() 或 poll() 的 I/O multiplexing 模型、信号驱动的 I/O 模型、通过 ioctl() 设置 FIOASYNC = O_ASYNC 的 Asynchronous I/O 模型。

https://www.tutorialspoint.com/unix_sockets/index.htm

https://www.binarytides.com/multiple-socket-connections-fdset-select-linux/

https://www.ibm.com/support/knowledgecenter/ssw_ibm_i_71/rzab6/coobd.htm

https://luminousmen.com/post/asynchronous-programming-blocking-and-non-blocking

http://www.differencebetween.net/language/difference-between-synchronous-and-asynchronous/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在 Qt 线程之间传递结构体,可以通过以下步骤实现: 1. 在结构体所在的头文件中包含 QDataStream 头文件。 2. 在结构体中添加一个函数,用于将结构体的数据写入到 QDataStream 中。 3. 在结构体中添加一个函数,用于从 QDataStream 中读取数据并将其填充到结构体中。 4. 在源线程中创建一个 QDataStream 对象,并使用结构体的写入函数将结构体数据写入到流中。 5. 将 QDataStream 对象转换为 QByteArray 对象,并使用信号和槽机制将 QByteArray 对象传递到目标线程。 6. 在目标线程中,将 QByteArray 对象转换为 QDataStream 对象,并使用结构体的读取函数将数据填充到结构体中。 下面是一个示例代码: ```c++ // MyStruct.h 文件 #include <QDataStream> struct MyStruct { int value1; QString value2; void writeToStream(QDataStream& stream) const { stream << value1 << value2; } void readFromStream(QDataStream& stream) { stream >> value1 >> value2; } }; Q_DECLARE_METATYPE(MyStruct) // 声明 MyStruct 类型,以便在信号和槽中使用 // MainWindow.cpp 文件 void MainWindow::onButtonClicked() { MyStruct data; data.value1 = 123; data.value2 = "Hello"; QByteArray buffer; QDataStream stream(&buffer, QIODevice::WriteOnly); data.writeToStream(stream); emit sendData(buffer); } void MainWindow::onDataReceived(const QByteArray& buffer) { MyStruct data; QDataStream stream(buffer); data.readFromStream(stream); // 处理接收到的数据 } // WorkerThread.cpp 文件 void WorkerThread::run() { qRegisterMetaType<MyStruct>(); // 注册 MyStruct 类型,以便在信号和槽中使用 connect(this, &WorkerThread::sendData, this, &WorkerThread::processData); } void WorkerThread::processData(const QByteArray& buffer) { MyStruct data; QDataStream stream(buffer); data.readFromStream(stream); // 处理接收到的数据 QByteArray newBuffer; QDataStream newStream(&newBuffer, QIODevice::WriteOnly); data.writeToStream(newStream); emit sendDataBack(newBuffer); } void WorkerThread::onDataSentBack(const QByteArray& buffer) { // 处理返回的数据 } ``` 在这个示例中,MyStruct 结构体包含两个成员变量:一个整数和一个字符串。writeToStream 和 readFromStream 函数分别用于将结构体数据写入到 QDataStream 中和从 QDataStream 中读取数据并填充到结构体中。在 MainWindow 类中,onButtonClicked 函数创建一个 MyStruct 实例,并将其写入到 QDataStream 中,然后将 QByteArray 对象发送到 WorkerThread 类中。在 WorkerThread 类中,processData 函数从 QByteArray 对象中读取 MyStruct 实例,并将其处理后,将其再次写入到 QByteArray 对象中,并将其发送回到 MainWindow 类中。在 onDataSentBack 函数中,MainWindow 类处理返回的数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值