文章目录
- 预备知识
- socket套接字
- UDP网络编程
一、预备知识
1.源IP地址和目的IP地址
IP地址:标识计算机在网络中的唯一性。
2.端口号
端口号:可以用来标识进程的唯一性。
网络通信的目的是让两台计算机上的两个进程在进行通信。
因为两台计算机之间进行数据的发送时,发送到计算机中的是进行网络通信的手段并不是目的,而真正进行网络通信的是两台计算机上面的某个应用之间的通信。
端口号(port)是传输层协议的内容.端口号是一个2字节16位的整数;端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;(全网唯一进程=IP地址+端口号)一个端口号只能被一个进程占用。
3.认识TCP/UDP协议
TCP协议
传输层协议有连接可靠传输面向字节流
UDP协议
传输层协议无连接不可靠传输面向数据报
4.网络字节序
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.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套接字
1.socket 常见API
#include <sys/types.h>
#include <sys/socket.h>
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
2.sockaddr结构
套接字的种类常见的可以分为三种:
- 域间套接字:存在于本地间通信。
- 网络套接字:跨主机进行网络通信同时也支持本地间通信。
- 原始套接字:可以跨越传输层(TCP/UDP)访问底层数据。
- 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结构体指针做为参数.
sockaddr_in结构
sockaddr结构in_addr结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;
将sockaddr看成是基类,将sockaddr_in和sockaddr_un看成是派生类。
in_addr转字符串的函数:
运行结果如下:
三、UDP网络编程
1.服务端实现
class UdpServer
{
public:
private:
// 一个服务器,一般必须需要ip地址和port(16位的整数)
uint16_t _port;
std::string _ip;
int _sock;
};
首先构造服务器时需要传入端口号和ip地址
一般来说端口号是指定的,一旦指定之后不能更改这里为8080,ip地址为服务器的地址。
UdpServer(const uint16_t &port,const std::string& ip=""):_port(port),_ip(ip),_sock(-1)
{}
通过调用ipconfig指令我们可以看到此时服务器的ip信息。
为了能够使服务器接受所有的网络请求,将服务器的ip地址绑定为INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。
初始化服务器
void initServer()
{
//1.创建套接字
_sock=socket(AF_INET,SOCK_DGRAM,0);
if(_sock<0){
std::cerr<<"socket err: "<<errno<<" : "<<strerror(errno)<<std::endl;
exit(2);
}
//bind绑定
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family=AF_INET;
local.sin_port=htons(_port);
local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
if(bind(_sock,(struct sockaddr*)&local,sizeof(local))<0)
{
std::cerr<<"bind error: "<<errno<<" : "<<strerror(errno)<<std::endl;
exit(2);
}
}
启动服务器
void Start()
{
char buffer[SIZE];
while(true){
struct sockaddr_in peer;
bzero(&peer,sizeof(peer));
socklen_t len=sizeof(peer);
ssize_t s=recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&(peer),&len);//阻塞读取
if(s>0){
buffer[s]=0;
std::string cli_ip=inet_ntoa(peer.sin_addr);
uint16_t cli_port=ntohs(peer.sin_port);
std::cout<<cli_ip<<"["<<cli_port<<"]#"<<buffer<<std::endl;
}
sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
}
}
利用recvfrom读取网络中的数据。
udp_server.cc
#include "udp_server.hpp"
#include <memory>
#include <cstdlib>
static void usage(std::string proc)//使用手册
{
std::cout<<"\nUsage: "<<proc<<"port\n"<<std::endl;
}
int main(int argc,char*argv[])
{
if(argc!=2){//命令行参数
usage(argv[0]);
exit(1);
}
uint16_t port=atoi(argv[1]);
std::unique_ptr<UdpServer> svr(new UdpServer(port));//利用智能指针创建服务器对象
svr->initServer();
svr->Start();
return 0;
}
2.客户端实现
class UdpClient
{
private:
uint16_t _port;
std::string _ip;
int sock;
};
客户端可以有无数个但是服务端只有一个,所以对于客户端来说只需要知道服务端的ip和端口号。
初始客户端
void initClient()
{
// 1.创建套接字
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
std::cerr << "socket err: " << errno << " : " << strerror(errno) << std::endl;
exit(2);
}
std::cout << "socket success: " << std::endl;
// 客户端不需要显示的绑定OS会随机分配
}
对于客户端的初始化不需要创建绑定ip和端口号,因为没有用户会对客户端进行网络请求,所以OS会随机分配
客户端启动
void run()
{
//填充服务器的ip
struct sockaddr_in server;
memset(&server, 0, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(_port);
server.sin_addr.s_addr = inet_addr(_ip.c_str());
char buffer[1024];
std::string message;
while (true)
{
std::cout << "请输入你的信息# ";
std::getline(std::cin, message);
//将数据发送到网络中
sendto(_sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(_sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
}
}
利用sendto向网络中发送数据
udp_client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <memory>
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " serverIp serverPort\n"
<< std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
std::string serverip=argv[1];
uint16_t serverport=atoi(argv[2]);
std::unique_ptr<UdpClient> ucli(new UdpClient(serverport,serverip));
ucli->initClient();
ucli->run();
return 0;
}