一、网络通讯程序
(1)服务端与客户端
网络通讯是指两台计算机中的程序进行传输数据的过程。
客户程序(客户端):指主动发起通讯的程序
服务程序(服务端/服务器):被动的等待,然后为向它发起通讯的客户端提供服务
通讯前,客户端必须提前知道服务端的IP地址和端口,而服务端不需要知道客户端的IP
(2)服务端与客户端基本流程
客户端流程:
①创建客户端的socket
②向服务器发起连接请求
③与服务端通信,发送一个请求报文后等待恢复,然后再发下一个请求报文
④关闭socket,释放资源
服务端流程:
①创建服务端的socket
②把服务端用于通信的IP和端口绑定到socket上
③把socket设置为可连接状态
④受理客户端的连接请求
⑤与客户端通信,接收客户端发过来的报文并恢复
⑥关闭socket,释放资源
二、网络编程的细节
(1)socket函数详解
创建socket:
int socket(int domain , int type , int protocol);
···domain参数 通讯协议族
PF_INET IPv4互联网协议族,是最普及最常用的
PD_INET6 IPv6互联网协议族
···type参数 数据传输类型
SOCK_STREAM 面向连接的socket:数据不会丢失,数据顺序不会错乱,双向通道
SOCK_DGRAM 无连接的socket:数据可能会丢失,数据顺序可能错乱,传输效率更高
···protocol参数 最终使用的协议
在IPv4网络协议家族中,
SOCK_STREAM的协议只有IPPROTO_TCP
SOCK_DGRAM的协议只有IPPROTO_UDP
本参数也可以填0
socket函数成功返回一个有效的socket,失败时返回-1,只要参数没填错,基本不会失败
(2)主机字节序与网络字节序
1)大端和小端
大端序:低位字节存放在高地址,高位字节存放在低地址
小端序:低位字节存放在低地址,高位字节存放在高地址
假如要存入16进制数 0x12345678
大端存储:低地址-> 0x12 0x34 0x56 0x78 ->高地址
小段存储:低地址-> 0x78 0x56 0x34 0x12 ->高地址
2)网络字节序
为了解决不同字节序的计算机之间传输数据的问题,约定采用网络字节序(大端序)
C语言提供了四个库函数,用于在主机字节序和网络字节序之间转换“
uint16_t htons(uint16_t hostshort); //2字节(16位)的整数
uint32_t htonl(unit32_t hostlong); //4字节(32位)的整数
uint16_t ntohs(uint16_t netshort);
uint32_y ntohl(unit32_t netlong);
h——host即主机 to——转换 n——network即网络 s——short l——long
3)IP地址和通讯端口
在计算机中,IPv4的地址用4字节整数存放,通讯端口用2字节的整数(0-65536)存放
例如: 192.168.190.134
大端:11000000 10101000 10111110 10000110
小端:10000110 10111110 10101000 11000000
在网络编程中,数据收发的时候有自动转换机制,不需要程序员手动转换
只有向sockaddr_in结构体成员变量填充数据时,才需要考虑字节序的问题
(3)用于存放协议端口和IP地址的结构体
1)sockaddr结构体
存放了协议、端口和地址信息,客户端和connect()函数和服务端的bind()函数需要这个结构体
struct sockaddr{
unsigned short sa_family; //协议族,固定填AF_INET
unsigned char sa_data[14]; //14字节的端口和地址
};
2) sockaddr_in 结构体
由于操作不方便,定义了等价的sockaddr_in结构体,大小与sockaddr相同,可以强制转换成sockaddr结构体。
struct sockaddr_in{
unsigned short sin_family;
unsigned short sin_port; //16位端口号,大端序,用htons转换
struct in_addr sin_addr; //32位IP地址
unsigned char sin_zero[8]; //不使用,为了保持与sockaddr长度一致而添加
};
3)in_addr 结构体
IP地址的结构体
struct in_addr{
unsigned int s_addr; //32位的IP地址,大端序
};
(4) 字符串IP与大端序IP的转换
C语言提供了几个库函数,用于字符串格式的IP和大端序IP的互相转换
1)把字符串格式IP转换成大端序IP后,赋值给sockaddr_in.in_addr.s_addr
in_addr_t inet_addr(const char *cp);
2)转换后的大端序IP填充到sockaddr_in.in_addr成员
int inet_aton(const char *cp , struct in_addr *inp);
3)把大端序IP转换成字符串格式的IP,用于在服务端程序中解析客户端的IP地址
char *inet_ntoa(struct in_addr in);
4)使用gethostbyname()函数,可以把域名/主机名/字符串IP 转换成 结构体
三、封装Socket
(1)将客户端封装成ctcpclient类
class ctcpclient //TCP通讯的客户端类
{
public:
int m_clientfd; //客户端的socket,-1表示未连接或者已断开,>=0表示有效的socket
string m_ip; //32位的服务器的ip/域名
unsigned short m_port; //16位的通讯端口
ctcpclient(): m_clientfd(-1) {} //构造函数
//连接函数,传入两个参数:服务器IP和通讯端口
//向服务端发起连接请求,成功返回true,失败返回false
bool connect(string &in_ip,const unsigned short in_port)
{
if(m_clientfd!=-1) //若socket已连接,直接返回失败
{ return false;}
m_ip = in_ip ; m_port = in_port; //将服务器的IP和端口保存到成员变量中
//第一步:创建客户端的socket
if((m_clientfd = socket(AF_INET,SOCK_STREAM,0))==-1)
{ return false ; }
//第二步:向服务器发起连接请求
struct sockaddr_in servaddr; //定义一个用于存放协议、端口和IP的结构体
memset(&servaddr , 0 , sizeof(servaddr)); //初始化结构体
servaddr.sin_family = AF_INET; //①协议族固定填入AF_INET
servaddr.sin_port = htons(m_port); //②指定服务器端的端口
struct hostent* h; //定义一个用于存放服务器IP地址的结构体的指针
if((h = gethostbyname(m_ip.c_str() )) == NULL)
{
::close(m_clientfd); //关闭客户端socket
m_clientfd = -1; //置为-1
return false;
}
memcpy(&servaddr.sin_addr , h->h_addr,h->h_length); //③指定服务端的IP
if(::connect(m_clientfd , (struct sockaddr *)&servaddr , sizeof(servaddr))==-1)//向服务端发起请求
{
::close(m_clientfd); //关闭客户端socket
m_clientfd = -1; //置为-1
return false;
}
return true;
}
//buffer用于存放接收到的报文的内容,maxlen为本次接收报文的最大长度
bool recv(string &buffer, const size_t maxlen)
{
buffer.clear(); //清空容器
buffer.resize(maxlen); //设置容器大小为maxlen
int readn= ::recv(m_clientfd, &buffer[0] , buffer.size() , 0); //操作buffer的内存
if(readn<=0) {buffer.clear(); return false; }
buffer.resize(readn); //重置buffer的实际大小
return true;
}
//向服务端发送报文,成功返回true,失败返回false
bool send(const string &buffer)
{
if(m_clientfd == -1) return false; //若socket未连接,直接返回失败
if((::send(m_clientfd , buffer.data() , buffer.size() , 0)) <=0) return false;
return true;
}
//断开与服务端的连接
bool close()
{
if(m_clientfd==-1) return false;
::close(m_clientfd);
return true;
}
~ctcpclient() { close(); }; //析构函数中调用成员函数close
};
将客户端类封装好后,便可在程序主函数中正常使用客户端的正常功能
int main(int argc,char *argv[])
{
if (argc!=3)
{
cout << "Using:./demo7 服务端的IP 服务端的端口\nExample:./demo7 192.168.101.138 5005\n\n";
return -1;
}
ctcpclient tcpclient;
if (tcpclient.connect(argv[1],atoi(argv[2]))==false) // 向服务端发起连接请求。
{
perror("connect()"); return -1;
}
// 第3步:与服务端通讯,客户发送一个请求报文后等待服务端的回复,收到回复后,再发下一个请求报文。
string buffer;
for (int ii=0;ii<10;ii++) // 循环3次,将与服务端进行三次通讯。
{
buffer="这是第"+to_string(ii+1)+"个用户,编号"+to_string(ii+1)+"。";
// 向服务端发送请求报文。
if (tcpclient.send(buffer)==false)
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
// 接收服务端的回应报文,如果服务端没有发送回应报文,recv()函数将阻塞等待。
if (tcpclient.recv(buffer,1024)==false)
{
perror("recv()"); break;
}
cout << "接收:" << buffer << endl;
sleep(1);
}
}
(2)将服务端封装成ctcpserver类
class ctcpserver //tcp通讯的服务端类
{
private:
int m_listenfd; //用于监听的socket,-1表示未初始化
int m_clientfd; //客户端连接上来的socket,-1表示客户端未连接
string m_clientip; //客户端的字符串格式IP
unsigned short m_port; //服务端用于通讯的端口
public:
ctcpserver():m_listenfd(-1),m_clientfd(-1){}//创建类对象时将监听socket和通讯socket初始为-1
//初始化服务器用于监听的socket
bool initserver(const unsigned short in_port)
{
//第一步:创建服务端的socket
if((m_listenfd=socket(AF_INET,SOCK_STREAM,0))==-1) return false;
m_port=in_port;
//第二步:把服务器用于通信的IP和端口绑定到socket上
struct sockaddr_in servaddr; //定义一个用于存放协议、端口和IP的结构体
memset(&servaddr,0,sizeof(servaddr)); //初始化结构体
servaddr.sin_family=AF_INET; //①协议族固定填入AF_INET
servaddr.sin_port=htons(m_port); //②填入服务端的通信端口
servaddr.sin_addr=htonl(INADDR_ANY); //③全部IP全都用于通讯
//绑定服务端的IP和端口
if(bind(m_listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))==-1)
{
close(m_listenfd); m_listenfd=-1 ; return false;
}
//第三步:把socket设置为可连接状态
if(listen(m_listenfd,5)== -1)
{
close(m_listenfd); m_listenfd=-1 ; return false;
}
return true;
}
//受理客户端的连接
bool accept()
{
struct sockaddr_in caddr; //客户端的地址信息
socklen_t addrlen=sizeof(caddr); //strcut sockaddr_in的大小
if((m_clientfd=::accept(m_listenfd,(struct sockaddr *)&caddr,&addrlen))==-1)
{ return false;}
m_clientip=inet_ntoa(caddr.sin_addr); //客户端的大端IP转换成字符串形式
return true;
}
//获取客户端的字符串形式IP
const string & clientip() const
{ return m_clientip; }
//向对端发送报文
bool send(const string &buffer)
{
if(m_clientfd == -1) return false;
if((::send(m_clientfd , buffer.data(), buffer.size() , 0))<=0) return false;
return true;
}
//接收对端的报文,buffer用来存放报文的内容
bool recv(string &buffer , const size_t maxlen)
{
buffer.clear(); //清空buffer
buffer.resize(maxlen); //设置容器大小
int readn=::recv(m_clientfd,&buffer[0],buffer.size(),0); //操作buffer的内存
if(readn<0) {buffer.clear();return false;}
buffer.resize(readn); //重置buffer的实际大小
return true;
}
//关闭监听socket
bool closelisten()
{
if(m_listenfd==-1) return false;
::close(m_listenfd);
m_listenfd=-1;
return true;
}
//关闭通讯socket
bool closeclient()
{
if(m_clientfd==-1) return false;
::close(m_clientfd);
m_clientfd=-1;
return true;
}
~ctcpserver(){closelisten();closeclient();}
};
将服务端的类封装好后,就可以在程序主函数中调用它的功能
int main(int argc,char *argv[])
{
if (argc!=2)
{
cout << "请输入指定端口"<<endl;
return -1;
}
ctcpserver tcpserver;
if (tcpserver.initserver(atoi(argv[1]))==false) // 初始化服务端用于监听的socket。
{
perror("initserver()"); return -1;
}
// 受理客户端的连接
// 如果没有已连接的客户端,accept()函数将阻塞等待。
if (tcpserver.accept()==false)
{
perror("accept()"); return -1;
}
cout << "客户端已连接(" << tcpserver.clientip() << ")。\n";
string buffer;
while (true)
{
// 接收对端的报文,如果对端没有发送报文,recv()函数将阻塞等待。
if (tcpserver.recv(buffer,1024)==false)
{
perror("recv()"); break;
}
cout << "接收:" << buffer << endl;
buffer="ok";
if (tcpserver.send(buffer)==false) // 向对端发送报文。
{
perror("send"); break;
}
cout << "发送:" << buffer << endl;
}
}
(3)客户端和服务端所需要的头文件
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
using namespace std;