目录
3、gethostbyname函数(域名/主机名/字符串IP转大端序)
一、创建socket
int socket (int domain, int type, int protocol);
成功返回一个有效的socket,失败返回-1,errno被设置
单个进程创建的socket数量受系统参数open files的限制。(ulimit -a查看)
参数:
- domain(通讯协议族):PF_INET 为IPv4互联网协议族(常用)
- type(数据传输类型):
SOCK_STREAM 为面向连接的socket:1)数据不会丢失;2)数据的顺序不会错乱;3)双向通道。
SOCK_DGRAM为无连接的socket:1)数据可能会丢失;2)数据的顺序可能会错乱;3)传输的效率更高。
protocol(最终使用的协议):
在IPv4网络协议家族中,数据传输方式为SOCK_STREAM的协议只有IPPROTO_TCP,数据传输方式为SOCK_DGRAM的协议只有IPPROTO_UDP。
例子:
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
二、TCP和UDP
1、TCP和UDP的区别
TCP
a)TCP面向连接,通过三次握手建立连接,四次挥手断开连接; 面试的重点
b)TCP是可靠的通信方式,通过超时重传、数据校验等方式来确保数据无差错,不丢失,不重复,并且按序到达;
c)TCP把数据当成字节流,当网络出现波动时,连接可能出现响应延迟的问题;
d)TCP只支持点对点通信;
e)TCP报文的首部较大,为20字节;
f)TCP是全双工的可靠信道。
UDP
a)UDP是无连接的,即发送数据之前不需要建立连接,这种方式为UDP带来了高效的传输效率,但也导致无法确保数据的发送成功;
b)UDP以最大的速率进行传输,但不保证可靠交付,会出现丢失、重复等等问题;
c)UDP没有拥塞控制,当网络出现拥塞时,发送方不会降低发送速率;
d)UDP支持一对一,一对多,多对一和多对多的通信;
e)UDP报文的首部比较小,只有8字节;
f)UDP是不可靠信道。
2、TCP保证自身可靠的方式
a)数据分片:在发送端对用户数据进行分片,在接收端进行重组,由TCP确定分片的大小并控制分片和重组;
b)到达确认:接收端接收到分片数据时,根据分片的序号向对端回复一个确认包;
c)超时重发:发送方在发送分片后开始计时,若超时却没有收到对端的确认包,将会重发分片;
d)滑动窗口:TCP 中采用滑动窗口来进行传输控制,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方不会再发送数据;
e)失序处理:TCP的接收端会把接收到的数据重新排序;
f)重复处理:如果传输的分片出现重复,TCP的接收端会丢弃重复的数据;
g)数据校验:TCP通过数据的检验和来判断数据在传输过程中是否正确。
3、UDP不可靠的原因
没有上述TCP的机制,如果校验和出错,UDP会将该报文丢弃。
4、TCP和UDP的使用场景
TCP 使用场景
TCP实现了数据传输过程中的各种控制,适合对可靠性有要求的场景。
UDP 使用场景
可以容忍数据丢失的场景:
- 视频、音频等多媒体通信(即时通信);
- 广播信息。
5、UDP能实现可靠传输吗?
如果用UDP实现可靠传输,那么,应用程序必须实现重传和排序等功能,非常麻烦。
三、主机字节序与网络字节序
- 大端序(Big Endian):低位字节存放在高位,高位字节存放在低位。
- 小端序(Little Endia):低位字节存放在低位,高位字节存放在高位。
为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序)。
C语言提供了四个库函数,用于在主机字节序和网络字节序之间转换:
uint16_t htons(uint16_t hostshort); // uint16_t 2字节的整数 unsigned short
uint32_t htonl(uint32_t hostlong); // uint32_t 4字节的整数 unsigned int
uint16_t ntohs(uint16_t netshort);
uint32_t ntohl(uint32_t netlong);
h host(主机);
to 转换;
n network(网络);
s short(2字节,16位的整数);
l long(4字节,32位的整数)
在计算机中,IPv4的地址用4字节的整数存放,通讯端口用2字节的整数(0-65535)存放。
四、网络通讯中的各种结构体
1、sockaddr结构体
存放协议族、端口和地址信息,客户端的connect()和服务端的bind()需要这个结构体
struct sockaddr {
unsigned short sa_family; // 协议族,与socket()函数的第一个参数相同,填AF_INET。
unsigned char sa_data[14]; // 14字节的端口和地址。
};
2、sockaddr_in结构体
sockaddr结构体是为了统一地址结构的表示方法,统一接口函数,但是,操作不方便。
所以定义了等价的sockaddr_in结构体,它的大小与sockaddr相同,可以强制转换成sockaddr。
struct sockaddr_in {
unsigned short sin_family; // 协议族,与socket()函数的第一个参数相同,填AF_INET。
unsigned short sin_port; // 16位端口号,大端序。用htons(整数的端口)转换。
struct in_addr sin_addr; // IP地址的结构体。192.168.101.138
unsigned char sin_zero[8]; // 未使用,为了保持与struct sockaddr一样的长度而添加。
};
struct in_addr { // IP地址的结构体。
unsigned int s_addr; // 32位的IP地址,大端序。
};
3、gethostbyname函数(域名/主机名/字符串IP转大端序)
根据域名/主机名/字符串IP获取大端序IP,用于网络通讯的客户端程序中。
struct hostent *gethostbyname(const char *name);
struct hostent {
char *h_name; // 主机名。
char **h_aliases; // 主机所有别名构成的字符串数组,同一IP可绑定多个域名。
short h_addrtype; // 主机IP地址的类型,例如IPV4(AF_INET)还是IPV6。
short h_length; // 主机IP地址长度,IPV4地址为4,IPV6地址则为16。
char **h_addr_list; // 主机的ip地址,以网络字节序存储。
};
#define h_addr h_addr_list[0] // for backward compatibility.
转换后,用以下代码把大端序的地址复制到sockaddr_in结构体的sin_addr成员中。
memcpy(&servaddr.sin_addr,h->h_addr,h->h_length);
四、字符串IP与大端序IP的转换(只能字符串IP转大端序)
C语言提供了几个库函数,用于字符串格式的IP和大端序IP的互相转换,用于网络通讯的服务端程序中。
- typedef unsigned int in_addr_t; // 32位大端序的IP地址。
- // 把字符串格式的IP转换成大端序的IP,转换后的IP赋给sockaddr_in.in_addr.s_addr。in_addr_t inet_addr(const char *cp);
- // 把字符串格式的IP转换成大端序的IP,转换后的IP将填充到sockaddr_in.in_addr成员。
int inet_aton(const char *cp, struct in_addr *inp);
- // 把大端序IP转换成字符串格式的IP,用于在服务端程序中解析客户端的IP地址。char *inet_ntoa(struct in_addr in);
五、socket通信的服务端类
头文件.h
class ctcpserver { private: int m_socklen; //结构体struct sockaddr_in的大小 struct sockaddr_in m_clientaddr; //客户端的地址信息 struct sockaddr_in m_servaddr; //服务端的地址信息 int m_listenfd; //服务端用于监听的socket int m_connfd; //客户端连接上来的socket public: ctcpserver():m_listenfd(-1),m_connfd(-1){} //构造函数 // 服务端初始化。 // port:指定服务端用于监听的端口。 //backlog:设置监听sock的第二个参数。 // 返回值:true-成功;false-失败,一般情况下,只要port设置正确,没有被占用,初始化都会成功。 bool initserver(const unsigned int port,const int backlog=5); // 从已连接队列中获取一个客户端连接,如果已连接队列为空,将阻塞等待。 // 返回值:true-成功的获取了一个客户端连接,false-失败,如果accept失败,可以重新accept。 bool accept(); //获取客户端的ip地址 // 返回值:客户端的ip地址,如"192.168.1.100"。 char *getip(); // 接收对端发送过来的数据。 // buffer:存放接收数据的缓冲区。 // ibuflen: 打算接收数据的大小。 // itimeout:等待数据的超时时间(秒):-1-不等待;0-无限等待;>0-等待的秒数。 // 返回值:true-成功;false-失败,失败有两种情况:1)等待超时;2)socket连接已不可用。 bool read(string &buffer, const int itimeout=0); bool read(Void *buffer,const int ibuflen,const int itimeout=0); // 向对端发送数据。 // buffer:待发送数据缓冲区。 // ibuflen:待发送数据的大小。 // 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。 bool write(const string &buffer); // 发送文本数据。 bool write(const void *buffer,const int ibuflen); // 发送二进制数据。 //关闭监听的socket,常用于多进程服务程序的子进程代码中。 void closelisten(); //关闭客户端的socket,常用于多进程服务程序的父进程代码中。 void closeclient(); ~ctcpserver(); // 析构函数自动关闭socket,释放资源。 }
服务端初始化函数,连接客户端函数,获得客户端ip函数
// 服务端初始化。 // port:指定服务端用于监听的端口。 //backlog:设置监听sock的第二个参数。 // 返回值:true-成功;false-失败,一般情况下,只要port设置正确,没有被占用,初始化都会成功。 bool ctcpserver::initserver(const unsigned int port,const int backlog) { // 如果服务端的socket>0,关掉它 if(m_listenfd > 0){ ::close(m_listenfd); m_listenfd=-1;} //创建监听的socket if((m_listenfd = socket(AF_INET,SOCK_STREAM,0))<=0) return false; // 忽略SIGPIPE信号,防止程序异常退出。 // 如果往已关闭的socket继续写数据,会产生SIGPIPE信号,它的缺省行为是终止程序,所以要忽略它。 signal(SIGPIPE,SIG_IGN); // 打开SO_REUSEADDR选项,当服务端连接处于TIME_WAIT状态时可以再次启动服务器, // 否则bind()可能会不成功,报:Address already in use。 int opt = 1; setsockopt(m_listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); memset(&m_servaddr,0,sizeof(m_servaddr)); m_servaddr.sin_family = AF_INET; m_servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //任意IP地址 m_servaddr.sin_port = htons(port); if(bind(m_listenfd,(struct sockaddr *)&m_servaddr,sizeof(m_servaddr)) != 0){ closelisten();return false; } if(listen(m_listenfd,backlog) != 0 ){ closelisten();return false; } return true; } // 从已连接队列中获取一个客户端连接,如果已连接队列为空,将阻塞等待。 // 返回值:true-成功的获取了一个客户端连接,false-失败,如果accept失败,可以重新accept。 bool ctcpserver::accept() { if(m_listenfd==-1) return false; int m_socklen = sizeof(struct sockaddr_in); if(m_connfd = ::accept(m_listenfd, (struct sockaddr *)&m_clientaddr,(socklen_t *)&m_socklen)) < 0) return false; return true; } // 获取客户端的ip地址。 // 返回值:客户端的ip地址,如"192.168.1.100"。 char *ctcpserver::getip() { return(inet_ntoa(m_clientaddr.sin_addr)); }
收发数据
收发数据设置超时时间,采用poll的超时机制。
tcp的发送和接收:
- 发送:把数据放到tcp的发送缓冲区。
- 接收:从tcp接收缓冲区中取数据。
分包和粘包:
- 分包:tcp报文的大小缺省是1460 字节,如果发送缓冲区中的数据超过1460字节,tcp将拆分成多个包发送,如果接收方及时的从接收缓冲区中取走了数据,看上去像就接收到了多个报文
- 粘包:tcp接收到数据之后,有序放在接收缓冲区中,数据之间不存在分隔符的说法,如果接收方没有及时的从缓冲区中取走数据看上去就象粘在了一起
同时为了避免出现TCP分包和粘包现象,发送文本数据时,使用报文长度(四字节整数)+报文内容来区分报文,eg:Hello world,先接收11,在接收Hello world。如果使用原生的send和recv函数,将发生分包和粘包的现象
发送数据
首先封装了原生send()函数为writen()函数
//send()函数:send函数的功能是把待发送的数据拷贝到发送缓冲区。 // 返回值是已拷贝的字节数,正常情况下,与待发送数据的字节数相同 // 如果发送缓冲区的空间不足,则返回本次已拷贝的字节数 //为了保证全部的数据被发送,应该循环调用send函数,直到全部的数据被发送完成。 // 向已经准备好的socket中写入数据。 // sockfd:已经准备好的socket连接。 // buffer:待发送数据缓冲区的地址。 // n:待发送数据的字节数。 // 返回值:成功发送完n字节的数据后返回true,socket连接不可用返回false。 bool writen(const int sockfd,const char *buffer,const size_t n) { int nleft=n; //剩余需要写入的字节数 int idx=0; //已经写入的字节数 int nwritten; //每次调用send()函数写入的字节数 while(nleft > 0){ if((nwritten = send(sockfd, buffer+idex, nleft,0)) <= 0) return false; nleft -= nwritten; idx += nwritten; } return true; }
发送数据代码:
// 发送文本数据。 bool ctcpserver::write(const string &buffer) { if (m_connfd==-1) return false; return(tcpwrite(m_connfd,buffer)); } // 发送二进制数据。 bool ctcpserver::write(const void *buffer,const int ibuflen) { if (m_connfd==-1) return false; return(tcpwrite(m_connfd,(char*)buffer,ibuflen)); } // 向socket的对端发送数据。 // sockfd:可用的socket连接。 // buffer:待发送数据缓冲区的地址。 // ibuflen:待发送数据的字节数。 // 返回值:true-成功;false-失败,如果失败,表示socket连接已不可用。 bool tcpwrite(const int sockfd,const string &buffer); // 写入文本数据。 bool tcpwrite(const int sockfd,const void *buffer,const int ibuflen); // 写入二进制数据。 // 发送文本数据。 bool tcpwrite(const int sockfd,const string &buffer) { if (sockfd==-1) return false; int buflen=buffer.size(); //先发送报头 if(writen(sockfd, (char*)&buflen,4)==false)return false; //再发送报文体 if(writen(sockfd, buffer.c_str(),buflen)==false)return false; return true; } bool tcpwrite(const int sockfd,const void *buffer,const int ibuflen) // 发送二进制数据。 { if (sockfd==-1) return false; if (writen(sockfd,(char*)buffer,ibuflen) == false) return false; return true; }
接收数据
首先封装了原生recv()函数为readn()函数:
// 从已经准备好的socket中读取数据。 // sockfd:已经准备好的socket连接。 // buffer:接收数据缓冲区的地址。 // n:本次接收数据的字节数。 // 返回值:成功接收到n字节的数据后返回true,socket连接不可用返回false。 bool readn(const int sockfd,char *buffer,const size_t n) { int nleft=n; // 剩余需要读取的字节数。 int idx=0; // 已成功读取的字节数。 int nread; // 每次调用recv()函数读到的字节数。 while(nleft > 0) { if ( (nread=recv(sockfd,buffer+idx,nleft,0)) <= 0) return false; idx=idx+nread; nleft=nleft-nread; } return true; }
接收数据代码:
// 接收文本数据。 bool ctcpserver::read(string &buffer,const int itimeout) { if (m_connfd==-1) return false; return(tcpread(m_connfd,buffer,itimeout)); } // 接收二进制数据。 bool ctcpserver::read(void *buffer,const int ibuflen,const int itimeout) { if (m_connfd==-1) return false; return(tcpread(m_connfd,buffer,ibuflen,itimeout)); } // 接收socket的对端发送过来的数据。 // sockfd:可用的socket连接。 // buffer:接收数据缓冲区的地址。 // ibuflen:本次成功接收数据的字节数。 // itimeout:读取数据超时的时间,单位:秒,-1-不等待;0-无限等待;>0-等待的秒数。 // 返回值:true-成功;false-失败,失败有两种情况:1)等待超时;2)socket连接已不可用。 bool tcpread(const int sockfd,string &buffer,const int itimeout=0);// 读取文本数据。 bool tcpread(const int sockfd,void *buffer,const int ibuflen,const int itimeout=0); // 读取二进制数据。 // 接收文本数据。 bool tcpread(const int sockfd,string &buffer,const int itimeout) // 接收文本数据。 { if(socket==-1) return false; //如果itimeout>0, 表示等待itimeout秒,如果itimeout秒后接收缓冲区中还没有数据,返回false if(itimeout>0){ //利用了poll的超时机制,poll会阻塞等待sockfd接收缓冲区事件,接收缓冲区有数据了,则唤醒sockfd读数据 struct pollfd fds; fds.fd=sockfd; fds.events=POLLIN; if( poll(&fds, 1, itimeout*1000) <= 0) return false; } // 如果itimeout==-1,表示不等待,立即判断socket的接收缓冲区中是否有数据,如果没有,返回false。 if (itimeout==-1) { struct pollfd fds; fds.fd=sockfd; fds.events=POLLIN; if ( poll(&fds,1,0) <= 0 ) return false; } //下面处理避免粘包 int buflen=0; //先读取报文长度,4个字节 if(readn(sockfd,(char*)&buflen,4)==false)return false; // 设置buffer的大小。 buffer.resize(buflen); // 再读取报文内容。 if(readn(sockfd,&buffer[0],buflen) == false) return false; return; } // 接收二进制数据。 bool tcpread(const int sockfd,void *buffer,const int ibuflen,const int itimeout) { if (sockfd==-1) return false; // 如果itimeout>0,表示需要等待itimeout秒,如果itimeout秒后还没有数据到达,返回false。 if (itimeout>0) { struct pollfd fds; fds.fd=sockfd; fds.events=POLLIN; if ( poll(&fds,1,itimeout*1000) <= 0 ) return false; } // 如果itimeout==-1,表示不等待,立即判断socket的缓冲区中是否有数据,如果没有,返回false。 if (itimeout==-1) { struct pollfd fds; fds.fd=sockfd; fds.events=POLLIN; if ( poll(&fds,1,0) <= 0 ) return false; } // 读取报文内容。 if (readn(sockfd,(char*)buffer,ibuflen) == false) return false; return true; }