网络套接字
一.网络字节序
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
- 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
- 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
- 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
二.端口号
端口号(port)是传输层协议的内容:
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用
三.socket
1.常见的API
- IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16位端口号和32位IP地址.
- IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
- socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;
2.封装UdpSocket
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<strings.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include "log.hpp"
extern Log log;
std::string defaultip="0.0.0.0";
uint16_t defaultport=8080;
const int size=1024;
enum{
SOCKET_ERR=1,
BIND_ERR
};
class UdpServer
{
public:
//初始化端口号,ip号
UdpServer(const uint16_t &port=defaultport,const std::string &ip=defaultip): port_(port),ip_(ip)
{}
void init()
{
//创建udp socket
sockfd_=socket(AF_INET,SOCK_DGRAM,0);
if(socket<0)//创建失败
{
log(Fatal,"socket create error: %d",sockfd_);
exit(SOCKET_ERR);
}
log(Info,"create socket sucess:%d",sockfd_);
//绑定端口号
struct sockaddr_in local;
//将该结构体内部清零
bzero(&local,sizeof(local));
//填充结构体
local.sin_family=AF_INET;//表明自己的结构体类型
local.sin_port=htons(port_);//绑定的端口号,需要保证我的端口号是网络字节序列(大端),因为要发送给对方,所以htos转换
local.sin_addr.s_addr=inet_addr(ip_.c_str());//绑定的ip,1.ting->uint_32 2.必须是网络序列的
//上面的全部定义在用户栈上,并没有与内核绑定
//绑定内核
int n=bind(sockfd_,(const struct sockaddr*)&local,sizeof(local));
if(n<0)//绑定失败
{
log(Fatal,"bind error,error:%s",strerror(errno));
exit(BIND_ERR);
}
log(Info,"bind sucess:%d",sockfd_);
}
void run()
{
isrunning=true;
while(isrunning)
{
char inbuffer[size];
struct sockaddr_in client;//客户端结构体
socklen_t len=sizeof(client);
ssize_t n=recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);
if(n<0)
{
log(Warning,"recvform err,err string:%s",strerror(errno));
continue;
}
inbuffer[n]=0;
//简单的数据处理
std::string info=inbuffer;
std::string echo_string="server echo"+info;
//将数据发回
sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(const sockaddr*)&client,len);
}
}
~UdpServer()
{}
private:
int sockfd_;//网络文件描述符
uint16_t port_;//端口号
std::string ip_;//ip号
bool isrunning;
};
可以使用netstat -naup查看是否启动成功。
四.地址转换函数
本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;
字符串转in_addr的函数:
in_addr转字符串的函数:
其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void*addrptr。
关于ntoa
man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.
那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.