Linux Socket 网络编程 阻塞与非阻塞 断线重连机制

三种非阻塞模式的方法:

(1) fcntl函数

int Mode = fcntl(sockfd, F_GETFL, 0);       //获取文件的Mode值
fcntl(sockfd, F_SETFL, Mode | O_NONBLOCK); //设置成非阻塞模式;

(2) recvfrom函数

int size = recvfrom(sockfd, (char *)recvbuffer, recvlength, MSG_DONTWAIT,(struct sockaddr*)&addr,(socklen_t*)&addlen);

参数flag不是设置为0,而是MSG_DONTWAIT

(3) ioctl ()函数

ioctl()函数,设置超时 还没细看

常见问题:

1. sendto返回值为-1

容易混淆的几点:

UDP

通过bind绑 定本机地址 (local addr)以及端口(local port), 实现从本机端口(local port)发送以及 监听 ;

通过connect指定目的地址(dst addr)以及目的端口(dst port), 实现目标地址的绑定;

UDP服务器绑定,recvfrom

接收端使用 bind() 函数,来完成地址结构与 socket 套接字的绑定,这样 ip、port 就固定了,发送端在 sendto 函数中指定接收端的 ip、port,就可以发送数据了。

接收方的绑定是绑定本地任意IP,特定端口;发送方需要知道接收方的IP地址,和接收方用的哪个端口接收,这是UDP的,而TCP不是这样的。TCP建立连接后,就已经知道客户端地址了。

1. TCP服务器

服务器一直都是监听的任意IP地址,端口号用于区分接收哪个客户端发来的数据。客户端代码向服务器哪个端口发,服务器就绑定哪个端口。

2. TCP客户端

3. UDP发送端

发送端不需要绑定操作,只需要知道IP和端口号,所以会有一个struct sockaddr_in addr;

发送,写的是对面的地址。

4. UDP接收端

相反,接收端肯定是需要绑定的了。就是监听,和TCP监听一样,监听任意IP地址,绑定发送端发送地址的端口号

一、UDP协议及其工作原理

(1) 特点

面向无连接的协议。

不保证数据一定能够到达对端,

不保证数据能够按照顺序到达对端

udp数据的可靠性需要应用层来保证。

UDP没有拥塞控制,网络出现的拥塞不会使源主机的发送速率降低

UDP支持一对一、一对多、多对一和多对多的交互通信

UDP消息头开销小,只有8个字节(TCP消息头共20个字节)

UDP相比较TCP更高效,牺牲效率

(2)udp和tcp的区别

协议 UDP TCP
是否面向连接
传输可靠性 不可靠 可靠
传输速度
应用场合 数据量少, 对可靠性要求不高 数据量大, 对可靠性要求较高

udp协议的格式非常简单,它只有8个字节的首部,后面的全是数据。

UDP是面向数据包的,对应用层数据既不合并也不拆分( 保留数据包边界,不粘包

二、UDP在Linux下的编程方式

编程流程

阻塞,非阻塞模式

服务端:

  1. 创建套接字
  2. 初始化socket地址
  3. 设置阻塞或者非阻塞模式
  4. 绑定服务端端口
  5. 发送或者接收
  6. 关闭套接字
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
using namespace std;
#define UDPPORT 60060
int main()
    int sockfd;
	struct sockaddr_in addr;
	sockfd = socket(AF_INET, SOCK_DGRAM, 0);
	memset(&addr, 0, sizeof(addr));
	addr.sin_family = AF_INET;
	addr.sin_port = htons(UDPPORT);
	addr.sin_addr.s_addr = htonl(INADDR_ANY);//客户端IP
    //绑定服务器UDP端口
	int bret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr));
	if (bret < 0)
		perror("bind fail");
		exit(1);
    char recvbuffer[1024];
    int recvlength = 0;
    recvlength = 1;
    int sendlength=0,actual_send_length=0;
    unsigned char sendbuffer[1024];
    int Mode = fcntl(sockfd, F_GETFL, 0);	   //获取文件的Mode值。
	fcntl(sockfd, F_SETFL, Mode | O_NONBLOCK); //设置成非阻塞模式;
    int addlen = sizeof(addr);
    while(1)
        int size = recvfrom(sockfd, (char *)recvbuffer, recvlength, 0,(struct sockaddr*)&addr,(socklen_t*)&addlen);
        if(size<0)
            perror("recv error");
            printf("%c\n", recvbuffer[0]);
            actual_send_length=sendto(sockfd,(char*)"I'm hear that you sending is:",29,0,(struct sockaddr*)&addr, sizeof(addr));
            //actual_send_length=sendto(sockfd,(char*)recvbuffer,size,0,(struct sockaddr*)&addr, sizeof(addr));
            if(actual_send_length<0)
                perror("send error");
                printf("send success\n");
        //usleep(500000);
}

客户端:

  1. 创建套接字
  2. 初始化socket地址
  3. 设置阻塞或者非阻塞模式
  4. 发送或者接收
  5. 关闭套接字
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
using namespace std;
#define UDPPORT 60060
int main()
    string cockpit_ip="127.0.0.1";
    int socketfd = 0;
    struct sockaddr_in addr;//服务器地址
    socketfd = socket(AF_INET, SOCK_DGRAM, 0);
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(UDPPORT);//服务器端口
    addr.sin_addr.s_addr = inet_addr("cockpit_ip.c_str()");//服务器IP
    int on=1;
    setsockopt(socketfd,SOL_SOCKET,SO_REUSEADDR | SO_BROADCAST,&on,sizeof(on));
    unsigned char sendbuffer[1024],recvbuffer[1024];
    int sendlength=0,actual_send_length=0,actual_recv_length=0,recvlength=0;
    sendbuffer[0]='A';
    sendlength = 1;
    recvlength = 1;
    //int Mode = fcntl(socketfd, F_GETFL, 0);	   //获取文件的Mode值。
	//fcntl(socketfd, F_SETFL, Mode | O_NONBLOCK); //设置成非阻塞模式;
    int addlen = sizeof(addr);
    while (1)
        actual_send_length = sendto(socketfd,(char*)sendbuffer,sendlength,0,(struct sockaddr*)&addr, sizeof(addr));
        if(actual_send_length<0)
            perror("send error");
            printf("actual_send_length = %d\n",actual_send_length);
        actual_recv_length = recvfrom(socketfd, (char *)recvbuffer, 29, 0,(struct sockaddr*)&addr, (socklen_t*)&addlen);
        if(actual_recv_length>0)
            printf("%s\n", recvbuffer);
            perror("recv error");
       //usleep(500000);
}

相关函数

服务器端: socket (), bind(), recvfrom()/sendto(), close();

客户端:socket(), sendto()/recvfrom(), close();

(1) socket()

#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocol);//创建socket

domain 表示底层协议族。其中IPv4用AF_INET(Protocol Family of Internet),IPv6用AF_INET6。

type 指定服务类型。服务类型主要有SOCK_STREAM(字节流服务)服务和SOCK_DGRAM服务(数据报服务)。对于TCP/IP协议族来说, SOCK_STREAM表 示传输层使用 TCP协议 SOCK_DGRAM 表示传输层使用 UDP协议。

protocol 在前两个参数构成的协议集合下,再选择一个具体的协议。 一般设置为0 ,表示使用默认协议。

socket()系统调用成功时返回一个socket文件描述符,失败则返回-1并设置errno。

(2)绑定bind()

#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr* my_addr,socklen_t addlen);//命名(绑定)socket

bind的作用是将未命名的sockfd文件描述符 指向my_addr所指的socket地址。 其中socket地址长度由参数addlen指出。

bind成功时返回0,失败则返回-1并设置errno。

其中struct sockaddr是通用的socket地址,而TCP/IP协议族有sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6,这里我们只介绍sockaddr_in:

struct sockaddr_in
    sa_family_t sin_family;  //地址族:AF_INET
    u_int16_t sin_port;      //端口号(要用网络字节序)
    struct in_addr sin_addr; //IPv4地址结构体
struct in_addr //IPv4地址结构体
    u_int32_t s_addr;  //IPv4地址(要用网络字节序)
};

(3) 转化函数

首先看端口号,两台主机之间要通过TCP/IP协议进行通信的时候需要调用相应的函数进行主机序 和网络序的转换。因为 主机字节序 一般为 小端模式 (Little-Endian),而 网络字节序 大端模式 (Big-Endian),也就是说两者的存储方式不同。所以我们介绍4个函数来完成主机字节序和网络字节序之间的转换:

#include<netinet/in.h>
unsigned long int htonl(unsigned long int hostlong);//主机字节序转网络字节序(32bit的长整型)
unsigned short int htonl(unsigned short int hostshort);//主机字节序转网络字节序(16bit的短整型)
unsigned long int ntohl(unsigned long int netlong);//网络字节序转主机字节序(32bit的长整型)
unsigned short int ntohs(unsigned short int netshort);//网络字节序转主机字节序(16bit的短整型

(4)3个IP地址转换函数

#include<arpa/inet.h>
in_addr_t inet_addr(const char* strptr);
int inet_aton(const char* cp,struct in_addr* inp);
char* inet_ntoa(struct in_addr in);

inet_addr函数将用点分十进制字符串表示的IPv4地址转化为用网络字节序整数表示的IPv4地址。

inet_aton函数完成inet_addr函数同样的功能,但是将转化结果存储于参数inp指向的地址结构中。该函数成功时返回1,失败返回-1。

inet_ntoa函数将用网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。

(5)数据读写

对文件的读写操作read和write同样适用于socket。但是socket编程接口提供了几个专门用于socket数据读写的系统调用,它们增加了对数据读写的控制。UDP与TCP不同,UDP通信没有连接的概念,所以我们每次读取数据都需要获取发送端的socket地址。所以UDP的读写函数比TCP的读写函数参数要多。

#include<sys/types.h>
#include<sys/socket.h>
int recvfrom(int sockfd,void* buf,size_t len,int flags,struct sockaddr* src_addr,socklen_t* addrlen);//读取sockfd上的数据
int sendto(int sockfd,const void* buf,size_t len,int flags,const struct sockaddr* dest_addr,socklen_t addrlen);//sockfd上写入数据

recvfrom/sendto系统调用也可以用于面向连接的socket数据读写,只需要把最后两个参数都设置为NULL以忽略发送端、接收端的socket地址(因为我们已经建立了连接,所以已经知道其socket地址了)。

通过 setsockopt() /getsockopt() 可存取指定socket的属性值

三 、深入UDP 数据收发(上)

① 问题:如何进行一对多的UDP数据发送?
② UDP通信中的广播
广播是向同一网络中的所有主机传输数据的方法

广播类型

直接广播:IP地址中除网络地址外,其余主机地址均设置为1(eg:192.168.1.255【网络地址 主机地址】)

本地广播:无需知道网络地址,使用255.255.255.255作为IP地址使用

区别:

本地广播数据不经过路由器寻址,直接发送到本地主机

四、TCP编程流程

4.1 编程流程

服务端

  1. 创建socket
  2. 初始化socket地址
  3. 绑定
  4. 监听
  5. 设置是否非阻塞
  6. 接收客户端连接
  7. 接收或发送
  8. 关闭
#include <netinet/tcp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <fcntl.h>
步骤1:创建一个TCP的socket
步骤2:绑定socket和端口号
步骤3:监听端口号
步骤4:接受请求连接
步骤5:读取字符
步骤6:关闭
using namespace std;
#define ServerPort 60050
string cockpit_ip ="127.0.0.1";
int main()
    int listenfd,connfd;//listenfd   socket描述符
	//创建一个TCP的socket
    if( (listenfd = socket(AF_INET,SOCK_STREAM,0)) == -1) {
        printf(" create socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
	struct sockaddr_in servaddr;
	 //先把地址清空,检测任意IP
    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(ServerPort);
	//地址绑定到listenfd
    if ( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1) {
        printf(" bind socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
    //监听端口号listenfd  最多10个等待
    if( listen(listenfd,10) == -1) {
        printf(" listen socket error: %s (errno :%d)\n",strerror(errno),errno);
        return 0;
    printf("====waiting for client's request=======\n");
    //accept 和recv,注意接收字符串添加结束符'\0'
	char buff[4096];
    int n;    
    //获取文件的Mode值
    int Mode = fcntl(listenfd, F_GETFL, 0);
	fcntl(listenfd, F_SETFL, Mode | O_NONBLOCK);   //设置成非阻塞模式;
    while(1)
	//接受请求连接
        if( (connfd = accept(listenfd, (struct sockaddr *)NULL, NULL))  == -1) {
            //printf(" accpt socket error: %s (errno :%d)\n",strerror(errno),errno);
            //return 0;
		//读取字符
        n = recv(connfd,buff,10,0);
        printf("recv msg from client:%s\n",buff);
    close(connfd);
	//关闭连接
    close(listenfd);
    return 0;   
}

客户端

  1. 创建socket
  2. 初始化socket地址
  3. 设置是否非阻塞
  4. 接收或发送
  5. 关闭
#include <netinet/tcp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <string>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <fcntl.h>
using namespace std;
#define ServerPort 60050
string cockpit_ip ="127.0.0.1";
int main()
    int len=0;		//总长度
	int buffer[65535];
    //1.建立套接字
	int TCP_Client_Sockfd;
	TCP_Client_Sockfd = socket(AF_INET, SOCK_STREAM, 0);//报式套接字 制定某个协议的特定类型为该TYPE的特定类型
	if (TCP_Client_Sockfd == -1)
		printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
    //2.建立连接
	//2.1设置要连接的服务器的地址
	struct sockaddr_in	Serveraddr;
	bzero(&Serveraddr, sizeof(struct sockaddr_in));
	Serveraddr.sin_family = AF_INET;//IPV4
	Serveraddr.sin_port = htons(ServerPort);//服务器应用程序端口
	Serveraddr.sin_addr.s_addr = inet_addr(cockpit_ip.c_str());//服务器IP
	//2.连接服务器  阻塞连接和非阻塞连接
	int Mode = fcntl(TCP_Client_Sockfd, F_GETFL, 0);        //获取文件的Mode值。
	fcntl(TCP_Client_Sockfd, F_SETFL, Mode | O_NONBLOCK);   //设置成非阻塞模式;
    int Connect_Time=0,Connect_Flag = 0;
    //TCP不断尝试连接
    while(1)
		Connect_Flag = connect(TCP_Client_Sockfd, (struct sockaddr*)(&Serveraddr), sizeof(sockaddr_in));
		if ( Connect_Flag == -1)
			printf("connect error\n");
			Connect_Time++;
			usleep(400000);//400ms
			if(Connect_Time>=40)
				break;
		else if(Connect_Flag == 0)
			printf("connect success\n");
			break;
    struct tcp_info info; 
    int len1=sizeof(info); 
    while(1)
		getsockopt(TCP_Client_Sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len1); 
		if((info.tcpi_state!=TCP_ESTABLISHED))  // 则说明断开  else 未断开
				close(TCP_Client_Sockfd);
				TCP_Client_Sockfd = socket(AF_INET, SOCK_STREAM, 0);//报式套接字 制定某个协议的特定类型为该TYPE的特定类型
				if (TCP_Client_Sockfd == -1)
					printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
				//2.建立连接
				//2.1设置要连接的服务器的地址
				struct sockaddr_in	Serveraddr;
				bzero(&Serveraddr, sizeof(struct sockaddr_in));
				Serveraddr.sin_family = AF_INET;//IPV4
				Serveraddr.sin_port = htons(ServerPort);//服务器应用程序端口
				Serveraddr.sin_addr.s_addr = inet_addr(cockpit_ip.c_str());//服务器IP
				//2.连接服务器  阻塞连接和非阻塞连接
				int Mode = fcntl(TCP_Client_Sockfd, F_GETFL, 0);        //获取文件的Mode值。
				fcntl(TCP_Client_Sockfd, F_SETFL, Mode | O_NONBLOCK);   //设置成非阻塞模式;
				while(1)
					connect(TCP_Client_Sockfd, (struct sockaddr*)(&Serveraddr), sizeof(sockaddr_in));
					getsockopt(TCP_Client_Sockfd, IPPROTO_TCP, TCP_INFO, &info, (socklen_t *)&len1);
					printf("getsockopt connect error\n"); 
                    usleep(100000);//100ms
					if((info.tcpi_state==TCP_ESTABLISHED))
						break;
        memcpy(buffer,"asdfg",5);
		if((info.tcpi_state==TCP_ESTABLISHED)) 
			len =send(TCP_Client_Sockfd,buffer,5,0);