C++(16)TCP一对多通信

563 阅读4分钟

TCP一对多通信

TCP一对多通信时服务器需要同时处理多个客户端的通信和新客户端的连接。

要实现必须使用多任务(多进程,多线程),主任务用来等待客户端连接,每连接上来一个客户端,开启一个新任务和客户端通信。

UDP通信

image-20220721233217138

1)服务器实现

1.获取socket描述符
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);  //用SOCK_DGRAM数据报套节字
2.准备通信地址(ip和端口号)
    struct sockaddr_in addr;
    //....  
3.绑定socket描述符和通信地址 ------ bind函数
    int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
    //参数就是描述符和地址,地址需要强转

//===============以下不一样==================
man recvfrom
4.和客户端通信(先接收) --------- recvfrom/sendto函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);
//参数中包括传输的数据和传输的地址
参数:
    flags - 接收标志
        0:阻塞方式
        MSG_DONTWAIT:非阻塞模式

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);
//参数中包括传输的数据和传输的地址       
参数:
    flags - 接收标志
        0:阻塞方式
        MSG_DONTWAIT:非阻塞模式
        
 5.不再通信关闭描述符             

2)客户端实现

1.获取socket描述符
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
2.准备服务器通信地址(ip和端口号)
    struct sockaddr_in addr;
    //.... 
3.和客户端通信(先发送) --------- recvfrom/sendto函数
4.不再通信关闭描述符      

注意:UDP通信通信数据中必须带有对方的通信地址,发送接收不能使用read/write,而要使用recvfrom/sendto

网络传输中文件的上传和下载

使用TCP的方式,在服务器和客户端之间进行文件的传输,传输文件时需要传输文件名,文件内容,设置传输完成的标志,处理出错的信息。

TCP三次握手/四次挥手

image-20220721233310049

while(getchar()!='\n'); ----------- 清除输入缓冲区的垃圾数据

IO多路复用

IO多路复用可以同时监控多个描述符,找出其中"活动的"描述符,所谓活动的就是指可以读/写/错误异常。使用IO多路复用可以使用以下接口。

select poll epoll

select多路复用的实现

1.准备要监控的描述符
    ...
2.将要监控的描述符放入对应的描述符集合(fd_set)
    void FD_CLR(int fd, fd_set *set);//从描述符集合中删除指定的描述符
    int  FD_ISSET(int fd, fd_set *set);//判断描述符集合中是否有该描述符
    void FD_SET(int fd, fd_set *set);//从描述符集合中添加指定的描述符
    void FD_ZERO(fd_set *set);//情况描述符集合
    
3.调用select函数监控对应的描述符集合
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
          fd_set *exceptfds, struct timeval *timeout);
参数:
    nfds - 监控的最大的描述符的值+1
    readfds - 传入传出参数,传入要监控的读描述符集合,传出活动的读描述符集合(是否可读)
    writefds - 传入传出参数,传入要监控的写描述符集合,传出活动的写描述符集合(是否可写)
    exceptfds  - 传入传出参数,传入要监控的异常描述符集合,传出活动的异常描述符集合(是否出错)
    timeout - 超时时间,不使用使用传NULL
返回值:
    成功返回活动的描述符个数,超时返回0,出错返回-1

4.处理活动的描述符                

网络超时

在工程开发中一般不设置无限等待,需要考虑到对方无响应的情况,将无限等待改为有限时间等待,网络超时就是等待有限的时间,如果有限时间内对方无响应就不再等待。

(1)多路复用的接口中自带超时处理(select poll epoll)

struct timeval tv;
tv.tv_sec = 3;//3s超时
tv.tv_usec = 0;//us
//select
if(select(maxfd+1, &set, NULL, NULL, &tv)<=0){
  printf("timeout!\n");
}

(2)通过socket属性来设置超时(read,recv,recvfrom,accept)

struct timeval tv;
tv.tv_sec = xxx;//s
tv.tv_usec = yyy;//us
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

(3)心跳检测

服务器和客户端在一定的时间间隔内(1s)发送一个代表保持连接的数据,表示客户端在线,如果能收到这个包,表示连接有效。

HTTP通信

http属于应用层的协议,同时方式通过请求,请求分为两类 --------- GET/POST

(1)http请求帧格式

请求行(request line)
    请求方法(GET) URL http协版本(HTTP/1.0) 回车换行
消息头部(header)
    key:value 回车换行(\r\n)
    .....
    空行  回车换行(\r\n)
请求正文
    ......       

(2)使用C语言编程访问http的API接口

1.解析域名,得到IP
    gethostbyname
2.创建socket连接API的IP和端口
    socket
    以API服务器的IP和端口构造地址
    connect
3.构造http请求
    按照http协议的要求构造http请求
4.发送http请求
    send
5.读取SPI服务器返回的数据
    recv                

设置地址重用

//在bind前调用
int val = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));

获取Linux本机IP

(1)使用getiifaddrs函数

该函数可以获取当前Linux系统中所有网络硬件的信息,调用该函数会传出一个ifaddrs的链表,链表中每一个节点就代表系统的一个网络硬件,其中有IP信息。

image-20220721234050984

image-20220721234055334

参考代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ifaddrs.h>
#include <sys/ioctl.h>
#include <net/if.h>

struct ifaddrs *ifAddrStruct = NULL;
struct ifaddrs *ifa = NULL;
void *tmpAddrPtr = NULL;   

//获取网络硬件信息
int res = getifaddrs(&ifAddrStruct);
if(res==-1){
    perror("getifaddrs");
    exit(-1);
}

//遍历网络硬件信息
for(ifa=ifAddrStruct; ifa!=NULL; ifa=ifa->ifa_next){
    //IPV4信息
    if(ifa->ifa_addr->sa_family==AF_INET){
        char buf[INET_ADDRSTRLEN] = {};
        tmpAddrPtr = &((struct sockaddr_in *)ifa->ifa_addr)->sin_addr;
        inet_ntop(AF_INET,  tmpAddrPtr, buf, INET_ADDRSTRLEN);
        printf("%s ip address %s\n", ifa->ifa_name, buf);
    }
}

freeifaddrs(ifAddrStruct);
return 0;

(2)可以使用ioctl去获取套接字的ip信息

先创建一个套接字(socket函数),调用ioctl获取网络硬件信息。

参考代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ifaddrs.h>
#include <sys/ioctl.h>
#include <net/if.h>    
    
   struct ifreq ifr[32];
	struct ifconf ifc;
	
	int sockfd = socket(AF_INET,SOCK_DGRAM,0);
	if(sockfd==-1){
		perror("socket");
		exit(-1);
	}
	
	ifc.ifc_len = sizeof(ifr);
	ifc.ifc_buf = (caddr_t)ifr;
	
    //获取系统网络硬件信息
	int res = ioctl(sockfd, SIOCGIFCONF, (char *)&ifc);
	if(res==-1){
		perror("ioctl");
		exit(-1);
	}
	
	int interface = ifc.ifc_len/sizeof(struct ifreq);
	while(interface--){
           //获取网络硬件信息中的地址参数
		res = ioctl(sockfd, SIOCGIFADDR, (char *)&ifr[interface]);
		if(res==-1){
			perror("ioctl");
			exit(-1);
		}
		
		printf("ip:%s\n",inet_ntoa(((struct sockaddr_in *)(&ifr[interface].ifr_addr))->sin_addr));
	}
	
	return 0;

启动服务