每个主机都有自己的IP地址,IP地址可以表示互联网上唯一的一台主机,而一个报文会携带两种IP地址,一种是IPB,一种是Mac地址,两种IP地址都有自己的源IP地址和目的IP地址,源MAC地址和目的Mac地址。
我们举个例子来看看他们之间的关系:
比如西游记中的唐僧去西天取经,会途径很多王国,而途径王国中,没到一个王国,都会给国王说我们去西天取经,问国王下一站是哪里,这就是查路由表的过程,而上一个王国和要准备去的下一个王国,就表示源Mac地址和目的Mac地址,而去西天就是源IP地址和目的IP地址。
所以目的IP地址一直不变,而Mac地址会一直变,目的IP就是为报文定制最终目的,路上根据该地址进行路径选择目的Mac,然后根据路径选择的结果来选择下一主机。
那么,两台主机通过数据传输来进行通信,而接受数据的主机有多个进程,这时就需要port(端口号)来定位需要接受数据的进程。
所以IP地址+port则构成进程的唯一性,所以把源IP+源port,目的IP+目的port称为socket通信
那么,进程PID也是唯一的,为什么不用PID呢?
用PID也是可以的,但是,我们计算机世界里一套题都各有不同的解法,如果用PID来标识进程的唯一性,不是所有进程都有通信,只有部分进程可能会进行网络通信,但是都用PID来标识,则无法区分,这是其一,最重要的是,如果用PID,而PID是OS层面进程管理的概念,也就是网络模块也要包含进程管理的部分,不然无法认识PID,所以就会增加OS中进程管理和网络管理的耦合度。
注意:一个进程可以绑定多个port,而一个port不能被多个进程绑定
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,,网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6, 然而,各种网络协议的地址格式并不相同
struct sockaddr_in:是通过网络来通信的sockaddr
struct sockaddr_un:是预间套接字,是通过本地来通信的sockaddr
struct sockaddr:是通用接口,想用网络就传in,用本地就传un,先对16位地址类型进行判断,是AF_INET,就在内部强转成in,是AF_UNIX,就强转成un
sockaddr 结构
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
sockaddr_in 结构
/* Structure describing an Internet socket address. */
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)];
};
虽然socket API的接口是sockaddr,但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址
in_addr结构
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
int socket(int domain, int type, int protocol);
int bind(int socket, const struct sockaddr* address, socklen_t address_len);
int listen(int socket, int backlog);
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
ssize_t recvfrom(int socket, void* buffer, size_t length, int flags,
struct sockaddr* address, socklen_t* address_len);
ssize_t sendto(int socket, const void* message, size_t length, int flags,
const struct sockaddr* dest_addr, socklen_t dest_len);
我们写一个UDP套接字来实现大小写转换
我们先创建一个封装退出码的头文件:err.hpp
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,
BIND_ERR
};
创建一个UDP服务端的头文件:udp_server.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "err.hpp"
namespace ns_server
{
const static uint16_t default_port = 8080;
using func_t = std::function<std::string(std::string)>; // 定义了一个函数,参数是string,返回值也是string
class UdpServer
{
public:
UdpServer(func_t cb, uint16_t port = default_port)
: service_(cb), port_(port)
{
std::cout << "server addr: " << port_ << std::endl;
}
void InitServer()
{
// 1、创建socket接口,打开网络文件
sock_ = socket(AF_INET, SOCK_DGRAM, 0); // 这个AF_INET是用来创建socket接口的
if (sock_ < 0)
{
std::cerr << "create socket error: " << strerror(errno) << std::endl;
exit(SOCKET_ERR); // 终止进程,0表示成功,非0表示失败
}
std::cout << "create socket error: " << sock_ << std::endl; // 3
// 2、给服务器指明IP地址和port端口号,初始化
struct sockaddr_in local; // 这个local在哪里定义呢?在用户空间的特点函数的栈帧上,不在内核中
bzero(&local, sizeof(local));
local.sin_family = AF_INET; // 初始化sockaddr_in的结构的,也可以写成PF_INET
local.sin_port = htons(port_); // 端口号这个字段会以报文的形式发到网络当中的,要主机转网络序列的
// 1.字符串风格的IP地址,转换成4字节int,1.1.1.1 -> uint32_t -> 能不能强制类型转换?不能,强制是把类型改了,这里要转化
// 2.需要将主机序列转化成为网络序列
// inet_addr把上面两件事都做了
// 3.云服务器,或者一款服务器,一般不需要指明某一个确定的IP
local.sin_addr.s_addr = INADDR_ANY; // 让我们的udpserver在启动的时候,可以bind本主机上的容易IP.
if (bind(sock_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind socket error: " << strerror(errno) << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket error: " << sock_ << std::endl;
}
void Start()
{
char buffer[1024];
while (true) // 服务器本质是一个死循环,随时可以使用
{
// 收,不断收消息
struct sockaddr_in peer; // 远端
socklen_t len = sizeof(peer); // 这里一定要写清楚,未来你传入的缓冲区大小
int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 从远端把数据传入buffer里
if (n > 0)
buffer[n] = '\0';
else
continue;
std::cout << "recv done.." << std::endl;
// 提取client信息
std::string clientip = inet_ntoa(peer.sin_addr); // 转成字符串风格的ip
uint16_t clientport = ntohs(peer.sin_port); // 从网络中来的,所以要转成主机序列
std::cout << clientip << " - " << clientport << "# " << buffer << std::endl;
// 做业务处理
std::string message = service_(buffer); // 收到的一条消息,把消息转给所以人
// 发,
// 网络套接字本质也是文件,当向文件中写入时,\0并不需要写到文件中,
// 因为\0是C语言中的规定,并不是网络的规定,网络对于客户端,也是一样,这就是为什么服务端要加\0的原因
sendto(sock_, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
}
}
~UdpServer()
{
}
private:
int sock_;
uint16_t port_;
func_t service_; // 我们的网络服务器刚刚解决的是网络IO收发的问题,要进行业务处理
};
}
创建一个UDP服务端的源代码文件,实现大小写转换:udp_server.cc
#include " udp_server.hpp"
#include
#include
#include
#include "err.hpp"
#include
using namespace ns_server;
using namespace std;
static void usage(string proc)
{
cout << "Usage:\n\t" << proc << "port" << endl; // 服务器必须永远有IP地址
}
// 上层的业务处理,不关心网络发送,只负责信息处理即可
// 上层的业务处理,和下层的网络进行了解耦,把结果再返回
std::string transactionString(std::string request) // request就是一个string
{
std::string result;
char c;
for (auto &r : request)
{
if (islower(r))
{
c = toupper(r);
result.push_back(c);
}
else
{
result.push_back(r);
}
}
return result;
}
//./ubp.server port
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<UdpServer> usvr(new UdpServer(transactionString, port));
usvr->InitServer(); // 服务器初始化
usvr->Start();
std::cout << "hello server" << std::endl;
return 0;
}
创建一个UDP客户端的源代码文件:udp_client.cc
#include
#include
#include "err.hpp"
#include
#include
#include
#include
#include
// 127.0.0.1:本地环回,就表示的就是当前主机,通常用来进行本地通信或者测试
// ./udp_client serverip serverport,客户端启动的时候,必须知道服务端的ip和port
// 客户端并不关心自己的ip和port,只需要别人找到自己就行了
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << "serverip serverport" << std::endl; // 服务器必须永远有IP地址
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(SOCKET_ERR);
}
// client 这里要不要bind呢?要的,socket通信的本质[clientip:clientport,server:serverport]
// client和server在通信时,我们双方地位是对等的,你给我发信息,我也要给你发信息
// 要不要自己bind呢?自己初始化,自己填一个结构体,然后再来bind,这就是自己bind
// 不需要自己bind,也不要自己绑定,OS自动给我们进行bind,bind叫系统调用,实际在正常操作时,除了用户直接填一些字段,服务器操作系统本身也是在帮你做工作的
// 为什么?电脑上,手机上有不同的客户端,这些客户端来自不同的企业,比如,抖音或淘宝等
// 所以,client的port要随机让OS分配,防止client出现启动冲突
// 为什么server要自己bind?
// 1、server的端口不能随意改变,众所周知且不能随意改变
// 2、服务器都是一家公司的,只要同一家公司,端口号需要统一规范化
// 明确server是谁
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str()); // 服务端不用明确ip,但是客户端要明确server对应的ip地址
//未来客户端不发数据,也有收数据的权利,也要改成多线程,下节课写
while (true)
{
// 先有消息,用户输入,比如平时刷抖音,点的赞等等
std::string message;
std::cout << "[我的服务器]#";
// std::cin >> message;
std::getline(std::cin,message);
// 发
// 什么时候bind
// 客户端也要有ip和port,所以一定要绑定,那么什么时候绑定呢?
// 在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP
// 1.bind 2.构建发送的数据报文
// message.c_str是发送的数据,作为的客户端,发给谁,发给服务端
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
// 收,接受
char buffer[2048];
struct sockaddr_in temp; // 客户端也会收到别人的消息,今天就一个服务器,给服务器发消息,一定会收到服务器转回来的消息,默认temp中的字段是server
// 未来你的客户端也会和别的客户端通信,或者别的服务器通信,可能收消息,来自不同的服务器,所以直接填充&temp
socklen_t len = sizeof(temp);
int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
我们先创建一个封装退出码的头文件:err.hpp
#pragma once
enum
{
USAGE_ERR=1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR
};
创建一个TCP服务端的头文件:tcp_server.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "err.hpp"
namespace ns_server
{
static const uint16_t defaultport = 8081;
static const int backlog = 32;
using func_t = std::function<std::string(const std::string &)>;
class TcpServer;//声明
class ThreadData
{
public:
ThreadData(int fd, const std::string &ip, const uint16_t &port,TcpServer *ts)
: sock(fd), clientip(ip), clientport(port),current(ts)
{
}
public:
int sock;
std::string clientip;
uint16_t clientport;
TcpServer* current;//声明后,初始化
};
class TcpServer
{
public:
TcpServer(func_t func, uint16_t port = defaultport) : func_(func), port_(port), quit_(true)
{
}
void initServer()
{
// 1.创建socket ,文件
listensock_ = socket(AF_INET, SOCK_STREAM, 0);//创建成功,是3号文件描述符
if (listensock_ < 0)
{
std::cerr << "create socket error" << std::endl;
exit(SOCKET_ERR);
}
// 2.绑定
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port_);
local.sin_addr.s_addr = INADDR_ANY; // 是全0,也可以写成htonl(INADDR_ANY)
if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind socket error " << std::endl;
exit(BIND_ERR);
}
// 3.监听
if (listen(listensock_, backlog) < 0)
{
std::cerr << "listen socket error " << std::endl;
exit(LISTEN_ERR);
}
}
void start()
{
// signal(SIGCHLD,SIG_IGN);//对信号进行忽略,忽略后,就不需要等了,不用写waitpid,推荐
// signal(SIGCHLD,handler);//以回收的方式,不推进
quit_ = false;//这个服务器不quit就一直运行
while (true)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
// 4.获取连接,accept
int sock = accept(listensock_, (struct sockaddr *)&client, &len);
if (sock < 0)
{
std::cout << "accept error" << std::endl;
continue;
}
// 提取client信息
std::string clientip = inet_ntoa(client.sin_addr); // client的ip
uint16_t clientport = ntohs(client.sin_port); // 从网络中来的,所以要转出网络序列,client的port
// 5.获取新连接成功,就开始进行业务处理
std::cout << "获取新连接成功: " << sock << " from " << listensock_ << " , "
<< clientip << " - " << clientport << std::endl; // 文件描述符sock from listensock,打印出来
// v3版本,多线程--原生多线程
// 1.要不要关闭不要的socket?多线程中,文件描述符表都是共享的,不需要,直接可以被外部所看到,关了,会影响我服务器
// 2.要不要回收线程?如何回收,会不会阻塞?要回收,可以分离,分离后,主线程不用关闭新线程了,主线程可以回过去继续获取连接
pthread_t tid;
ThreadData *td = new ThreadData(sock, clientip, clientport,this);
pthread_create(&tid, nullptr, threadRoutine, td);
}
}
static void *threadRoutine(void *args)
{
pthread_detach(pthread_self());//分离后,主线程不用关闭新线程了,主线程可以回过去继续获取连接
ThreadData *td = static_cast<ThreadData *>(args);
td->current->service(td->sock,td->clientip,td->clientport);
delete td;
}
void service(int sock, const std::string &clientip, const uint16_t &clientport)
{
std::string who = clientip + "-" + std::to_string(clientport);
char buffer[1024];
while (true)
{
ssize_t s = read(sock, buffer, sizeof(buffer) - 1); // 读数据,大于0,读取成功
if (s > 0)
{
buffer[s] = 0;
std::string res = func_(buffer); // 读到数据以后,回调一下,回调出去,再回调回来,把结果给我,就可以进行特定的处理
std::cout << who << ">>> " << res << std::endl;
write(sock, res.c_str(), res.size()); // 对对方的客户端进行写回的操作
}
else if (s == 0)
{
// 对方把连接关闭了
close(sock);
std::cout << who << " quit,me too " << std::endl;
break;
}
else
{
close(sock);
std::cout << "read error: " << strerror(errno) << std::endl;
break;
}
}
}
~TcpServer()
{
}
private:
uint16_t port_;
int listensock_;
bool quit_; // 表示服务器是否启动
func_t func_;
};
}
创建一个TCP服务端的源代码文件:tcp_server.cc
#include "tcpServer.hpp"
#include
using namespace std;
using namespace ns_server;
static void usage(string proc)
{
std::cout << "Usage:\n\t" << proc << "port\n"
<< std::endl; // 服务器必须永远有IP地址
}
std::string echo(const std::string &message)
{
return message;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
unique_ptr<TcpServer> tsvr(new TcpServer(echo, port));
tsvr->initServer();
tsvr->start();
return 0;
}
创建一个TCP客户端的源代码文件:tcp_client.cc
#include
#include
#include
#include
#include
#include
#include
#include "err.hpp"
using namespace std;
static void usage(std::string proc)
{
std::cout << "Usage:\n\t" << proc << "serverip serverport\n"
<< std::endl; // 服务器必须永远有IP地址
}
int main(int argc, char *argv[])
{
// 准备工作
if (argc != 3)
{
usage(argv[0]);
exit(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
// 1.创建套接字
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
std::cerr << "create socket error" << strerror(errno) << std::endl;
exit(SOCKET_ERR);
}
// 客户端要不要bind?要
// 要不要自己bind?不要,因为client要让OS自动给用户进行bind
// 要不要listen?不要,因为客户端永远都是连别人的,永远都是别人处于listen状态
// 要不要获取连接,不需要,获取连接都是服务器做的事情
// 那么客户端需要什么
// 2.发起连接
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
//serverip.c_str()这是字符串风格的ip,转成&server.sin_addr 4字节的ip地址
inet_aton(serverip.c_str(), &server.sin_addr); // 不能是INADDR_ANY,因为客户端要知道服务器的ip,是字符串风格的点分十进制的ip地址,要转成4字节
int cnt = 5;
while (connect(sock, (struct sockaddr *)&server, sizeof(server)) != 0) // 不等于0,失败,sock向server发消息
{
sleep(1);
cout << "正在给你尝试重连,重连次数还有: " << cnt-- << endl;
if (cnt <= 0)
break;
}
if (cnt <= 0) // 连接失败
{
cerr << "连接失败" << endl;
exit(CONNECT_ERR);
}
char buffer[1024]; // 读的时候,就要有自己的缓冲区
// 3.连接成功
while (true)
{
string line;
cout << "Enter>> ";
getline(cin, line);
write(sock, line.c_str(), line.size());//给服务器,写回去
ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
cout << "server echo >>>" << buffer << endl;
}
else if (s == 0)
{
cerr << "server quit" << endl;
break;
}
else
{
cerr << "read error: " << strerror(errno) << endl;
break;
}
}
close(sock);
return 0;
}