文章目录
- 预备知识
- socket(网络套接字)编程接口
- 简单的UDP网络程序
- 增加多用户可以互相通信
预备知识
网络字节序
大端存储:数据的高字节内容保存在内存的低地址处,数据的低字节内容保存在内存的高地址处
小端存储:数据的高字节内容保存在内存的高地址处,数据的低字节内容保存在内存的低地址处
内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏
移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
网络字节序和主机字节序的转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
socket(网络套接字)编程接口
// 创建 socket 文件描述符
int socket(int domain, int type, int protocol);
// 绑定端口号
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,然而, 各种网络协议的地址格式并不相同.
简单的UDP网络程序
socket函数
int socket(int domain, int type, int protocol);
参数说明:
domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM,如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。
protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数说明:
sockfd:创建套接字返回的文件描述符
addr:网络相关的属性
传入的addr的长度
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
sin_port:表示端口号
sin_family:表示协议家族
sin_addr:表示IP地址
字符串转in_addr
in_addr_t inet_addr(const char *cp);
服务器绑定
class udpServer
{
udpServer(uint16_t port, string ip = "") : _port(port), _ip(ip)
{
}
void init()
{
//1. 创建套接字
_sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd < 0)
{
logmessage("error");
exit(1);
}
logmessage("socket create sucessfully");
// 2. 创建ip与端口号
struct sockaddr_in local;
memset(&local,0,sizeof(local));
local.sin_family = AF_INET;
local.sin_port=htons(_port);
// INADDR_ANY表示让服务器端计算机上的全部网卡的IP地址均可以做为服务器IP地址
local.sin_addr.s_addr = _ip.empty() ? htonl(INADDR_ANY) :
inet_addr(_ip.c_str());
socklen_t len = sizeof(local);
//3.连接
if(bind(_sockfd,(struct sockaddr*)&local,len) == -1)
{
logmessage("bind error");
exit(2);
}
logmessage("bind sucessfully");
}
private:
int _sockfd; //套接字
uint16_t _port; // 端口号
string _ip; // ip地址
};
服务器读取用户发来的数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
buf:读取数据的存放位置。
len:期望读取数据的字节数。
flags:读取的方式。一般设置为0,表示阻塞读取。
src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。
服务器接收用户信息
message.hpp
struct message
{
/* data */
message()
{
_time=(uint64_t)time(nullptr);
}
friend ostream& operator<<(ostream& out,const message& mess);
char _name[10];
char _message[1024];
uint64_t _time;
};
ostream& operator<<(ostream& out,const message& mess)
{
out<<"时间:"<<mess._time<<"用户名:"<<mess._name<<endl;
out<<mess._message<<endl;
return out;
}
void start()
{
message mess;
struct sockaddr_in client;
socklen_t len = sizeof(client);
memset(&client,0,sizeof(client));
while(1)
{
ssize_t s = recvfrom(_sockfd,&mess,sizeof(mess),0,(struct sockaddr*)&client,&len);
if(s < 0)
{
logmessage("recvfrom error");
continue;
}
logmessage("recvfrom sucess");
cout<<mess;
}
}
客户端创建套接字
class udpClient
{
public:
udpClient(const string &serip, uint16_t serport) : _serip(serip), _serport(serport)
{
//创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
}
private:
int _sockfd; // 套接字
string _serip; // 服务器ip
uint16_t _serport; // 服务器port
};
为什么客户端不需要绑定
所谓的"不需要",指的是: 不需要用户自己bind端口信息!因为OS会自动给你绑定,推荐这么做
sendto函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
参数说明:
sockfd:对应操作的文件描述符。表示将数据写入该文件描述符索引的文件当中。
buf:待写入数据的存放位置。
len:期望写入数据的字节数。
flags:写入的方式。一般设置为0,表示阻塞写入。
dest_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
addrlen:传入dest_addr结构体的长度。
客户端发送消息
void communicate()
{
message mess;
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(_serport);
server.sin_addr.s_addr = inet_addr(_serip.c_str());
cout<<"请输入你的名字"<<endl;
cin>>mess._name;
while (1)
{
cout<<"请输入你要发的消息"<<endl;
cin>>mess._message;
sendto(_sockfd, &mess, sizeof(mess), 0,
(struct sockaddr *)&server, sizeof(server));
}
}
效果:
增加多用户可以互相通信
class udpServer
{
public:
void start()
{
message mess;
struct sockaddr_in client;
socklen_t len = sizeof(client);
memset(&client,0,sizeof(client));
while(1)
{
ssize_t s = recvfrom(_sockfd,&mess,sizeof(mess),0,
(struct sockaddr*)&client,&len);
if(s < 0)
{
logmessage("recvfrom error");
continue;
}
logmessage("recvfrom sucess");
cout<<mess;
// 给其他用户聊天发送信息
messageRoutine(mess);
string ip_port;
ip_port+=to_string(ntohl(client.sin_addr.s_addr));
ip_port+=to_string(ntohs(client.sin_port));
users.insert({ip_port,client});
}
}
private:
void messageRoutine(const message& mess)
{
for(auto user:users)
{
sendto(_sockfd,&mess,sizeof(mess),0,
(struct sockaddr*)&(user.second),sizeof(user.second));
}
}
private:
int _sockfd; //套接字
uint16_t _port; // 端口号
string _ip; // ip地址
unordered_map<string,struct sockaddr_in> users; // 所有的用户
};
class udpClient
{
public:
udpClient(const string &serip, uint16_t serport) : _serip(serip), _serport(serport)
{
//创建套接字
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
}
void communicate()
{
pthread_t t;
pthread_create(&t,nullptr,ReadMessage,(void*)&_sockfd);
pthread_detach(t);
message mess;
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(_serport);
server.sin_addr.s_addr = inet_addr(_serip.c_str());
cout<<"请输入你的名字"<<endl;
cin>>mess._name;
cout<<"开始通信"<<endl;
while (1)
{
cin>>mess._message;
sendto(_sockfd, &mess, sizeof(mess), 0, (struct sockaddr *)&server, sizeof(server));
}
}
~udpClient()
{
}
private:
static void* ReadMessage(void* args)
{
int socket = *(int*)args;
message mess;
struct sockaddr_in tmp;
socklen_t len=sizeof(tmp);
while(1)
{
recvfrom(socket,&mess,sizeof(mess),0,(sockaddr*)&tmp,&len);
cout<<mess<<endl;
}
}
private:
int _sockfd; // 套接字
string _serip; // 服务器ip
uint16_t _serport; // 服务器port
};
效果: