《网络江湖盟主令:套接字九式破阵图谱》上篇
目录
一、前言
二、正文
1.预备知识
1.1 理解源IP地址和目的IP地址
1.2 认识端口号
1.3 理解“端口号”和“进程id”
1.4 理解源端口号和目的端口号
1.5 认识TCP协议
1.6 认识UDP协议
1.7 网络字节序
2. socket编程接口
2.1 socket 常见API
2.2 sockaddr结构
● sockaddr 结构
● sockaddr_in 结构编辑
● in_addr结构编辑
3. 简单的UDP网络程序
3.1 封装UdoSocket
3.2 UDP通用服务器
3.3 实现英译汉服务器
3.3 UDP通用客户端
3.4 实现英译汉客户端
3.5 地址转换函数
● 字符串转in_addr的函数:
● in_addr转字符串的函数:
三、结语
一、前言
本文为大家带来网络编程套接字的学习,笔者会分上下两部分来讲解。在本节介绍到以下内容:
● 认识IP地址, 端口号, 网络字节序等网络编程中的基本概念;
● 学习socket api的基本用法;
● 能够实现一个简单的udp客户端/服务器;
二、正文
1.预备知识
1.1 理解源IP地址和目的IP地址
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址,就如同我们上一节讲过的唐僧师徒四人西天取经,起点是东土大唐,终点是西天。
但是我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上, 但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析,因为一个机器上可能有很多个服务端,就像我们手机上有很多app,我们要把传给机器的数据给哪个服务端呢?这就离不开端口号了。
1.2 认识端口号
端口号(port)是传输层协议的内容
● 端口号是一个2字节16位的整数;
● 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
● IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
● 一个端口号只能被一个进程占用
1.3 理解“端口号”和“进程id”
在讲完端口号后,相信有的小伙伴们肯定有疑问,既然端口号是用来标识一个进程,为啥不用我们之前在学习系统编程的的pid呢,pid不是也可以用来标识唯一一个进程。那么进程id和端口号两者是怎样的关系呢,为啥要有端口号呀?
①不是所有的进程都要进行网络通信,但是所有的进程都要有pid
②实现系统和网络的解耦
1.4 理解源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁";
由于我们需要客户端与服务端进行通信,因此通信双方就存在源端口号和目的端口号,要注意的是一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。很简单,前者是一个进程可以收到多个端口给的消息,但是后者是一个端口要给多个进程消息,在单次通信看来,前者还是一对一,但是后者就是一对多了。
1.5 认识TCP协议
● 传输层协议
● 有连接
● 可靠传输
● 面向字节流
1.6 认识UDP协议
● 传输层协议
● 无连接
● 不可靠传输
● 面向数据报
1.7 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
● 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
● 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
● 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
● TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
● 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据; 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
● 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
● 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
● 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 ;
● 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
2. socket编程接口
2.1 socket 常见API
// 创建 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.2 sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.
● 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结构体指针做为参数;
注:强转成struct sockaddr的结构体类型,通过读取16位地址类型就能识别出选择不同的sockaddr,提高的编码的简便性
● sockaddr 结构
● sockaddr_in 结构
虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主 要有三部分信息: 地址类型, 端口号, IP地址.
● in_addr结构
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;
3. 简单的UDP网络程序
接下来我们来实现一个简单的UDP网络程序——简单的英译汉功能
注:代码中会用到地址转换函数,在下面会讲到
3.1 封装UdoSocket
udp_socket.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
class UdpSocket {
public:
UdpSocket() : fd_(-1) {}
//创建套接字文件
bool Socket()
{
fd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (fd_ < 0)
{
perror("socket");
return false;
}
return true;
}
//关闭套接字文件
bool Close()
{
close(fd_);
return true;
}
//绑定套接字文件
bool Bind(const std::string& ip, uint16_t port)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return false;
}
return true;
}
//接受网络中通信请求
bool RecvFrom(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL)
{
char tmp[1024 * 10] = {0};
sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t read_size = recvfrom(fd_, tmp,sizeof(tmp) - 1, 0, (sockaddr*)&peer, &len);
if (read_size < 0)
{
perror("recvfrom");
return false;
}
// 将读到的缓冲区内容放到输出参数中
buf->assign(tmp, read_size);
if (ip != NULL)
{
*ip = inet_ntoa(peer.sin_addr);
}
if (port != NULL)
{
*port = ntohs(peer.sin_port);
}
return true;
}
//向指定ip发送请求
bool SendTo(const std::string& buf, const std::string& ip, uint16_t port)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0, (sockaddr*)&addr,
sizeof(addr));
if (write_size < 0)
{
perror("sendto");
return false;
}
return true;
}
private:
int fd_;
};
3.2 UDP通用服务器
udp_server.hpp
#pragma once
#include "udp_socket.hpp"
// C 式写法
// typedef void (*Handler)(const std::string& req, std::string* resp);
// C++ 11 式写法, 能够兼容函数指针, 仿函数, 和 lamda
#include
typedef std::function Handler;
class UdpServer {
public:
UdpServer() {}
~UdpServer() {}
bool Start(const std::string& ip, uint16_t port, Handler handler) {
// 1. 创建 socket
assert(sock_.Socket());
// 2. 绑定端口号
bool ret = sock_.Bind(ip, port);
if (!ret)
{
return false;
}
// 3. 进入事件循环
for (;;)
{
// 4. 尝试读取请求
std::string req;
std::string remote_ip;
uint16_t remote_port = 0;
bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);
if (!ret)
{
continue;
}
std::string resp;
// 5. 根据请求计算响应
handler(req, &resp);
// 6. 返回响应给客户端
sock_.SendTo(resp, remote_ip, remote_port);
printf("[%s:%d] req: %s, resp: %s
", remote_ip.c_str(), remote_port,
req.c_str(), resp.c_str());
}
sock_.Close();
return true;
}
private:
UdpSocket sock_;
};
3.3 实现英译汉服务器
以上代码是对 udp 服务器进行通用接口的封装. 基于以上封装, 实现一个查字典的服务器就很容易了.
dict_server.cc
#include "udp_server.hpp"
#include
#include
std::unordered_map g_dict;
void Translate(const std::string& req, std::string* resp)
{
auto it = g_dict.find(req);
if (it == g_dict.end()) {
*resp = "未查到!";
return;
}
*resp = it->second;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
printf("Usage ./dict_server [ip] [port]
");
return 1;
}
// 1. 数据初始化
g_dict.insert(std::make_pair("hello", "你好"));
g_dict.insert(std::make_pair("world", "世界"));
g_dict.insert(std::make_pair("c++", "最好的编程语言"));
// 2. 启动服务器
UdpServer server;
server.Start(argv[1], atoi(argv[2]), Translate);
return 0;
}
3.3 UDP通用客户端
udp_client.hpp
#pragma once
#include "udp_socket.hpp"
class UdpClient {
public:
UdpClient(const std::string& ip, uint16_t port) : ip_(ip), port_(port)
{
assert(sock_.Socket());
}
~UdpClient()
{
sock_.Close();
}
bool RecvFrom(std::string* buf)
{
return sock_.RecvFrom(buf);
}
bool SendTo(const std::string& buf)
{
return sock_.SendTo(buf, ip_, port_);
}
private:
UdpSocket sock_;
// 服务器端的 IP 和 端口号
std::string ip_;
uint16_t port_;
};
3.4 实现英译汉客户端
#pragma once
#include "udp_socket.hpp"
#include "udp_client.hpp"
#include
int main(int argc, char* argv[])
{
if (argc != 3)
{
printf("Usage ./dict_client [ip] [port]
");
return 1;
}
UdpClient client(argv[1], atoi(argv[2]));
for (;;)
{
std::string word;
std::cout << "请输入您要查的单词: ";
std::cin >> word;
if (!std::cin)
{
std::cout << "Good Bye" << std::endl;
break;
}
client.SendTo(word);
std::string result;
client.RecvFrom(&result);
std::cout << word << " 意思是 " << result << std::endl;
}
return 0;
}
3.5 地址转换函数
本节只介绍基于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。
代码示例:
三、结语
到此为止,本文关于网络编程套接字(上)的内容到此结束了,如有不足之处,欢迎小伙伴们指出呀!
关注我 _麦麦_分享更多干货:_麦麦_-CSDN博客
大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下期见!