Socket编程基础与QT的TCP通信
网络编程的重要性
- 单台计算机能做的工作非常有限,只有实现多台计算机的互联互通,才能提供更加强大的功能。实现多台计算机之间的互通互连具有极大的实用价值。由于现在网络的不断发展完善,通过网络实现计算机的互通互连是一件简单但及其重要的事。当前各种应用基本上都需要实现联网功能,即学会网络编程是一个程序员的基本要求。
- 现在上网如此简单,为什么还要学习网络编程。感觉上网简单不代表不需要学习网络编程知识,相反,这说明了网络编程的适用范围之广、影响之深,更加说明了网络编程的重要性。
Socket基础知识
- 提到网络编程就不得不说著名的TCP/IP协议。即网络节点由一个ip地址代表,加上端口号,就能标示某台机器中的某个进程。该协议的实用性得到了广泛的验证,并得到了极好的支持,即基本上大多数需要联网的机器都支持该协议。为了能有效使用该协议实现各进程间的数据交互,我们通常使用一个名为Socket(嵌套字)的编程接口进行网络编程。
- Socket起源于Unix,故其被设计成一个“文件”,它支持文件的各种操作:创建,打开,读写,关闭等。在程序中可以将其视为一种特殊的文件,可以通过该文件实现不同机器之间的数据交互。
- Socket不仅仅支持TCP协议,它是一个接口,通过该接口可以使用许多协议实现网络通信。具体使用什么协议,由Socket创建函数的参数确定。TCP协议只是被Socket所支持的协议的一种,不是唯一。不过本片博客是以TCP协议为例,初步学习认识Socket编程,进而学习如何进行网络编程。
Socket通讯流程
- Socket通信流程如下图:
创建Socket:创建Socket即得到一个Socket描述符,该描述符唯一标示一个Socket,以后对该Socket的操作大多需要使用其对应的Socket描述符号。
- 使用函数为
int socket(int domain, int type, int protocol);
。 domain为协议域,常见的有AF_INET(ipv4协议)、AF_INET6(ipv6协议)、AF_LOCAL (用于同一台机器上不同进程间进行通信)、AF_ROUTE(用于程序与系统内核进行数据交互)等。不同协议域的Socekt决定了其地址类型,在后面必须赋予正确类型的地址,该Socket才能用于通信。
- 在TCP通信中,协议域选择AF_INET(ipv4协议)或者AF_INET6(ipv6协议),地址为ip地址和端口号的组合。
type为Socket类型,常见的有SOCK_STREAM (流式嵌套字,主要用于面向连接的数据传输,如TCP协议)、 SOCK_DGRAM(数据包式嵌套字,主要用于非连接可靠数据传输,如UDP协议) 、 SOCK_RAW (原始网络协议式嵌套字,用于调用更多网络协议的数据报,如ICMP报文等,还支持修改报文头等)、 SOCK_PACKET(网络驱动式嵌套字,该嵌套字直接将数据从网卡传给用户,即只支持网卡协议,不会安装其它协议进行数据的预处理) 、 SOCK_SEQPACKET (可靠的数据包式嵌套字,即以数据包的形式交互数据,但是提供了确认机制以保障其可靠性)。
- 在TCP通信中,类别一般选择SOCK_STREAM。
protocol为此Socket所使用的协议常见的有IPPROTO_TCP(TCP协议)、IPPTOTO_UDP(UDP协议)、IPPROTO_SCTP(SCTP协议,可靠的面向控制的一个协议)、IPPROTO_TIPC(TIPC协议,一种适用于高可信网络中或者集群中的协议,即可靠性有网络本身确定,这样的网络一般成本较高、范围较小) 。该参数不是前面两个参数的任意组合,只能是支持协议域的协议的Socket类型,即前两个参数的某些正确的组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合 。一般此值填0(表示选择type值的默认协议)
- 在TCP通信中,协议一般填IPPROTO_TCP。
TCP创建服务器监听Socket的一个示例
int listenfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
- 使用函数为
绑定地址。当得到一个Socket后,需要根据该Socket的协议域绑定一个正确类型的地址给该Socket。只有绑定了地址,该Socket才能具有实际意义,才能用于通信。绑定地址这个操作是用户主动绑定还是由操作系统自己进行绑定会有不同的结果。当用户自己主动绑定,该Socket地址就可写入代码。在C/S模式中,有一个的服务器监听Socket,这个Socket就需要用户主动绑定地址,这样客户端通信Socket才能知道连接地址(即,服务器监听Socket和服务器通信Socket以及客户端通信Socket本质上没有差别,只是是否主动绑定地址而其使用方式不同)。而客户端的地址一般都是系统自己绑定,如果用户自己绑定地址,可能地址被其它进程使用,从而发生错误。
- 使用函数为
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
。 - sockfd表示想绑定地址的Socket描述字,即表示想给哪一个Socket绑定地址,一般为Socket()函数的返回值。
addr表示地址,不同的协议域的地址类型不一样,要保证地址类型的正确性。
TCP协议中,地址类型为ip号和端口号的组合类型,较为友好的类型为
struct sockaddr_in
该类型中有数据成员.sin_family
保存协议域,.sin_addr.s_addr保存
ip地址,.sin_port
保存端口号,一个该类型实例化例子struct sockaddr_in my_addr;//创建实例 my_addr.sin_family=AF_INET;//指定协议域为ipv4协议域 my_addr.sin_addr.s_addr=htonl(INADDR_ANY);//本地所有地址 my_addr.sin_port=htons(8000);//端口号为8000
- addrlen表示地址类型的长度,一般使用sizeof()获取。
TCP协议中的地址绑定的一个完整示例
struct sockaddr_in my_addr;//创建实例 my_addr.sin_family=AF_INET;//指定协议域为ipv4协议域 my_addr.sin_addr.s_addr=htonl(INADDR_ANY);//本地所有地址 my_addr.sin_port=htons(8000);//端口号为8000 bind(listenfd, (struct sockaddr *)&my_addr, sizeof(my_addr))
- 使用函数为
监听Socket使其变为被动类型的Socket。socket()创建的是默认为主动型的socket,而listen()函数可以将转化为被动型socket,即需要等待其他socket连接它,而被动性Socket的地址一般是由用户主动绑定的,这样其它Socket才能知道以什么地址连接它。
- 使用的函数为
int listen(int sockfd, int backlog);
- sockfd即想被转化为被动型Socket的Socket描述字。
backlog表示可存储的连接数量大小,即可以存储多少个等待被用户取走使用的连接。不同协议或者嵌套字类型的连接含义不一样。
- TCP的被动型Socket将维护两个队列(SYN QUEUE和ACCEPT QUEUE),SYN QUEUE队列保存未完成三次握手的连接,ACCEPT QUEUE队列保存完成三次握手的连接。而队列的大小由listen()函数的第二个参数确定。SYN QUEUE队列的元素在完成三次握手后会被插入ACCEPT QUEUE中(前提是ACCEPT QUEUE还有空间),当ACCEPT QUEUE的元素数量达到最大值时,该Socket将暂停新的连接,直到ACCEPT QUEUE的元素被取走后,才能接受新的连接。
- 监听Socekt的示例
listen(listenfd,10);//可存储连接数为10
- 使用的函数为
其它Socket申请连接。当由Socket被置为监听状态后,其它Socket就可以申请连接以便后续形成通信对。这样才能进行双方的数据交互。
- 使用函数
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen)
- sockfd为申请连接到监听Socket的Socket描述字。
- servaddr为监听Socket的地址。该地址一般由用户主动绑定,故可以被用户赋值以申请连接。同时地址类型要求如上,由该Socket的协议域确定。
申请连接示例
int clientSocket=Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); connect(clientSocket,my_addr,sizeof(my_addr));
- 使用函数
提取连接形成通信对。当被动型Socket被其它Socket连接成功并被用户申请提供服务时,将会从该被动型Socket的已完成队列中取出一个已完成连接并返回一个新的Socket以提供服务。申请连接的Socket与这个返回的Socket进行通信,而不是直接与监听Socket进行通信。即在C/S模式中:经过主动绑定地址并置为监听状态的Socket(通常被称为服务器端监听Socket)不会参与直接通信,而是返回新的Socket(通常被称为服务器通信Socket),其返回的Socket才被用来参与通信。
- 使用的函数为
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- sockfd为被置为监听状态的Socket的描述字。
- addr表示申请连接的Socket的地址,这个地址内容一般由申请连接的Socket所在系统自己分配绑定,而监听的Socket所返回的用于通信的Socket需要知道申请连接的Socket的地址。当然,当对申请连接的Socket的地址不感兴趣(即在代码中不会使用),可置为UNLL,这样通信Socket所需信息由系统自己赋予,不用用户自己赋予。同时地址类型要求如上,由该Socket的协议域确定。
- addrlen表示addr的长度,一般由sizeof获取。当addr的值为NULL时,这个值也为NULL。
形成通信对的示例
int communicationSk=Socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); communicationSk=accept(int sockfd, NULL, NULL);//申请连接的地址相关操作由系统自己完成
- 使用的函数为
交互数据。通过上述操作,将会形成一对一对的通信对(Socket对),在通信对间就可以进行数据交互。以实现网络通信的效果。但是交互的数据如何组织、是什么含义、如何使用,由用户确定,可以使用出名的协议(如http),也可以自己定义。
发送数据。即将特定区域的数据由Socket发送给对端的Socket。
- 使用函数为
ssize_t write (int socketfd, const void *buffer, size_t size)
。 - socketfd。想发送数据的Socket的描述字。
- buffer。存储着想发送的数据的存储区域。该区域存储的数据的数据格式、内容由用户自己决定。
- size。表示存储区域的大小。可由sizeof确定。
发送数据示例
/* *通过Socket实例 clientSocket将my_data发送给clientSocket的对端*Socket, */ std::string my_data('hello world'); write(clientSocket,&my_data,sizeof(my_data));
- 使用函数为
接受数据。当Socket被对端发送了数据后,可以将接受到的数据赋予到特定的区域,以可以被用户标记并自行使用。
- 使用函数
ssize_t read (int socketfd, void *buffer, size_t size)
- socketfd,被发送了数据的Socket的描述字。
- buffer,保存所接受到的数据的存储区域,通过buffer用户可以标示所接受到的数据,并在后续进行相关使用。
- size,从Socket读取的数据的大小。一般和buffer大小相同。
接受数据示例
/* *通过Socket实例communicationSk将my_data的数据内容存储在*my_server_data */ char my_server_data[12]=[]{'\0'}; read(communicationSk,my_server_data,11);
- 使用函数
关闭Socket。由于Socket是一种特殊的文件,和其它文件一样,当不需要时需要被关闭。关闭Socket有两种常用的关闭方式。这两种关闭方式效果不太一样(是否影响其它进程的读写、是否会回收Socket资源),应用环境不太一样(是否需要在半连接状态单方面传输数据)。其间的差别需要深入学习,这里只做了简短的介绍。
一种是close()函数,当多个进程共享同一个Socket时,该Socket会维护一个引用计数(即有多少个进程共享该Socket),在某个进程中调用close函数时,引用计数会减一,同时关闭该Socket在该进程的读和写,即该socket在该进程中用户不能主动使用读写函数(该进程的缓冲区中未发送或者接受的数据会先处理完,再根据引用计数是否为0选择是否关闭连接,但是在close函数后用户不能在该进程中再调用读写函数进行额外的数据收发。但是其它共享该Socket的进程还是可以使用该Socket进行读写操作)。只有引用计数为0时,即所有共享该Socket的进程都不需要该Socket时,才会进行断开连接操作(如TCP就会进行4次握手断开连接操作)并进行Socket的各种资源的回收(如回收Socket的缓冲区等)。即close只是关闭一个进程中的Socket的读写,当所有进程都close后会回收资源。
- 使用函数
int close(int fd);
- fd代表想要关闭的Socket的描述字。
关闭socket示例
close(communicationSk) /*由于只有一个进程使用communicationSk,调用这个函数后,系统会在处理 *好缓冲区中的数据后,进行4次握手断开连接操作,如果有多个进程使用该*Socket时,此时不会断开连接,直到所有进程都调用主动关闭才会断开连接 */
- 使用函数
一种是shutdown()。该函数可以指定关闭读还是关闭写,还是都关闭。但是是影响所有共享该Socket的进程的。即不管该Socket的引用计数的数量是否为0,所有进程都不能在该函数后调用读或者写或者都不能(具体情况由其指定的关闭方向确定)。但是Socket的资源不会回收,只有程序结束或者调用close后且引用计数为0时才会回收资源。
- 使用函数
int shutdown(int sockfd, int howto)
。 - sockfd要关闭读写的Socket描述字
howto表示的关闭方向
- howto=SHUT_RD(0) 时,关闭的是Socket的读功能,即所有共享该Socket的进程都无法通过该socket读取其它socket发送过来的数据。
- howto=SHUT_WD(1) 时,关闭的是Socket的写功能,即所有共享该Socket的进程都无法通过该socket发送数据给其它socket。
- howto=SHUT_RDWD(2) 时,关闭的是Socket的读写功能,即所有共享该Socket的进程都无法通过该socket进行数据的收发。
- 使用函数
- C/S模式通信流程总结:服务器端:创建服务器监听Socket(创建、绑定地址、监听、有连接时返回服务器端通信Socket),客户端:申请连接(创建、连接)。服务器中有两种Socket(这两种Socket本质没有差别,只是对Socket进行了不同操作从而形成不同种类的Socket)。同时Socket的关闭也需要根据不同的需求选择不同的关闭方式。
QT的TCP通讯流程
QT有其独特的信号槽机制,其TCP通信中也引入的信号槽机制。QT中,使用QTcpServer类创建服务器监听Socket。使用QTcpSocket类创建用于通信的Socket。其通信流程大致如下:
- 创建监听Socket(即在服务器端实例化一个QTcpServer对象)
调用QTcpServer对象的listen方法进行监听,等待连接的到来。
- 当有连接申请到来时,QTcpServer对像会发送一个newConnection信号,关联该信号到一个槽函数(这个槽函数中一般会调用nextPendingConnection方法,该方法会返回一个QTcpSocket对象,即服务器端的通信Socket)。
创建客户端通信Socket(即客户端实例化一个QTcpSocket对象)
- 客户端调用QTcpSocket对象的connectToHost方法申请连接。
交互数据。当通过上述步骤创建了通信对后,就可以进行数据交互了。
- 当有数据到某个QTcpSocket对象时,该QTcpSocket对象会发送readyRead信号。关联该信号到一个槽函数(这个槽函数一般会调用readAll或者其它读方法,然后对读取的数据进行后续处理)
- 调用QTcpSocket的write函数发送数据。
关闭连接。当不需要TCP连接时,客户端调用QTcpSocket对象的disconnectFromHost方法申请断开连接。
- 此时服务器通信Socket会发送disconnected信号,将该信号连接相应的断开连接槽函数。
QT的TCP通信实现多个客户端连接服务器的一种方法
- 对单个客户端不用继承QTcpServer(除非想在服务器使用单例模式,以确保只有一个服务器实例,则需要继承QTcpServer)和QTcpSocket,但对多个客户端时有一个处理方法就是:继承上述两个类,因为需要知道是哪一个socket发来的信息,故需要将socket的信息处理函数封装一下,同时由于派生了QTcpSocket,如果需要使用派生类,就需要使用QTcpServer的虚函数incomingConnection(这是一个当有客户端申请连接时,就会执行的函数)来产生自定义的socket,故QTcpServer也需要进行派生以重写虚函数incomingConnection。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。