我们把数据从A主机发送到B主机,是目的吗?不是,真正通信的不是这两个机器!其实是这两台机器上面的软件(人)
数据有IP(公网)
标识一台唯一的主机,用谁来标识各自主机上客户或者服务进程的唯一性呢?
为了更好的表示一台主机上服务进程的唯一性,我们采用端口号port
,标识服务器进程,客户端进程的唯一性!
端口号(port)是传输层协议的内容:
ip地址(主机全网唯一性) + 该主机上的端口号,标识该服务器上进程的唯一性
IP保证全网唯一,port保证在主机内部的唯一性
主机上对应的服务进程,在全网中是唯一的一个进程。
网络通信的本质:其实就是进程间通信
进程已经有pid,为什么要有port呢?
进程+port–>网络服务进程
底层OS如何根据port找到指定的进程:OS内部采用hash方案,在OS内部维护了一个基于端口号的哈希表,key就是端口号,value就是task_struct的地址。有这个端口号就可以找到PCB,继而找到文件描述符表,文件描述符对象,对象找到了那么这个文件的缓冲区也就能找到,然后就可以将数据拷贝到缓冲区,最后就相当于我们将网络数据放到了文件中,如同读文件一样就将数据读上去了。
一个进程可以绑定多个端口号;但是一个端口号不能被多个进程绑定;
理解源端口号和目的端口号:
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。 就是在描述 “数据是谁发的,要发给谁”;
认识TCP(Transmission Control Protocol 传输控制协议)协议:
认识UDP(User Datagram Protocol 用户数据报协议)协议:
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
函数作用:
将数据在不同字节序之间进行转换。
函数的详细介绍:
htonl()
函数:将一个 32 位无符号整数(unsigned int)从本地字节序转换为网络字节序(大端字节序)。htons()
函数:将一个 16 位无符号整数(unsigned short)从本地字节序转换为网络字节序(大端字节序)。ntohl()
函数:将一个 32 位无符号整数(unsigned int)从网络字节序(大端字节序)转换为本地字节序。ntohs()
函数:将一个 16 位无符号整数(unsigned short)从网络字节序(大端字节序)转换为本地字节序。// 创建 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);
按道理要实现上述三种套接字应该要三套不同的接口,但是设计者只设计了一套接口,通过不同的参数解决所有网络或其他场景下的通信问题
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket。然而,各种网络协议的地址格式并不相同
netinet/in.h
中,IPv4地址用sockaddr_in
结构体表示,包括16位地址类型,16位端口号和32位IP地址。AF_INET、AF_INET6
。 这样,只要取得某种sockaddr
结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。socket API
可以都用struct sockaddr *
类型表示,在使用的时候需要强制转化成sockaddr_in
; 这样的好处是程序的通用性,可以接收IPv4,IPv6,以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;sockaddr 结构:
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结构:
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
in_addr
用来表示一个IPv4
的IP
地址。其实就是一个32位的整数。
显示当前户籍UDP连接状况与端口号的使用情况:
sudo netstat -nuap
int socket(int domain, int type, int protocol);
函数作用:
用于创建一个新的网络套接字的系统调用。
函数参数:
domain
参数指定了网络协议族:
AF_INET
表示 IPv4 协议AF_INET6
表示 IPv6 协议AF_UNIX
表示 Unix 域协议type
参数指定了套接字的类型
SOCK_STREAM
表示面向连接的流套接字SOCK_DGRAM
表示无连接的数据报套接字SOCK_RAW
表示原始套接字。protocol
参数指定了使用的协议
IPPROTO_TCP
表示 TCP 协议IPPROTO_UDP
表示 UDP 协议0
时,系统会根据指定的 domain 和 type 参数选择一个默认的协议。这通常是最常用的协议,例如对于 AF_INET 和 SOCK_STREAM 的组合,通常使用的协议是 TCP(即 IPPROTO_TCP)。使用 socket() 函数的一般流程如下:
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
inet_aton()
函数:将一个字符串形式的 IP 地址转换为一个二进制形式的 IP 地址。如果转换成功,函数返回非零值,否则返回零。inet_addr()
函数:将一个字符串形式的 IP 地址转换为一个 32 位的整数,该整数表示为网络字节序。如果转换成功,函数返回一个非零值(即返回一个网络字节序表示的 IP 地址),否则返回 INADDR_NONE。inet_network()
函数:将一个字符串形式的 IP 地址的网络部分转换为一个 32 位的整数,该整数表示为网络字节序。inet_ntoa()
函数:将一个二进制形式的 IP 地址转换为一个字符串形式的 IP 地址。注意,该函数返回的是一个指向静态缓冲区的指针,因此不要将其作为返回值传递给其他函数。inet_makeaddr()
函数:根据网络号和主机号创建一个 IP 地址。inet_lnaof()
函数:从一个二进制形式的 IP 地址中提取主机号部分。inet_netof()
函数:从一个二进制形式的 IP 地址中提取网络号部分。sockaddr_in
是一个 IPv4 地址结构体,用于存储 IP 地址和端口号信息。在使用套接字函数时,通常需要将地址信息存储在 sockaddr_in 结构体中,并将其作为参数传递给函数
struct sockaddr_in local; // 定义了一个变量,栈,用户
bzero(&local, sizeof(local));//用于将指定的内存区域清零
local.sin_family = AF_INET;//将结构体成员 sin_family 设置为 AF_INET,表示使用 IPv4 地址族
local.sin_port = htons(_port);
//结构体成员 sin_port 设置为要使用的端口号,使用 htons() 函数将端口号转换为网络字节序(大端字节序)
local.sin_addr.s_addr = inet_addr(_ip.c_str());
//将结构体成员 sin_addr 设置为要使用的 IP 地址,使用 inet_addr() 函数将 IP 地址转换为网络字节序(大端字节序)
//在实际开发中,可以使用 inet_pton() 函数将字符串形式的 IP 地址转换为一个 struct in_addr 类型的结构体,该结构体包含了 IP 地址的二进制表示
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);
函数作用:
用于将一个本地地址(IP 地址和端口号)与一个套接字关联起来的函数
函数参数:
socket
参数是一个指定了套接字的文件描述符。addr
参数是一个指向 struct sockaddr
类型的结构体的指针,该结构体包含了要绑定的本地地址信息。addrlen
参数是 addr 结构体的长度函数返回值:
返回值为 0 表示绑定成功,-1 表示绑定失败,错误码保存在 errno 变量中。
ssize_t recvfrom(int socket, void *restrict buffer, size_t length,
int flags, struct sockaddr *restrict address,
socklen_t *restrict address_len);
函数作用:
用于从已连接或未连接的套接字接收数据的函数
函数参数:
socket
参数是指定了要接收数据的套接字的文件描述符。buf
参数是一个指向接收数据的缓冲区的指针。len
参数是缓冲区的大小。flags
参数是一组标志位,可以用来指定接收数据的行为。为0表示默认address
参数是一个指向 struct sockaddr 类型的结构体的指针,用于存储发送数据的远程地址。addrlen
参数是 src_addr 结构体的长度。函数返回值:
函数返回值为接收到的数据的字节数,如果没有数据可用,则返回 0。如果发生错误,则返回 -1,错误码保存在 errno 变量中
ssize_t sendto(int socket, const void *message, size_t length,
int flags, const struct sockaddr *dest_addr,
socklen_t dest_len);
函数作用:
用于向已连接或未连接的套接字发送数据的函数
函数参数:
socket
参数是指定了要发送数据的套接字的文件描述符。buf
参数是一个指向要发送数据的缓冲区的指针。len
参数是要发送数据的字节数。flags
参数是一组标志位,可以用来指定发送数据的行为。dest_addr
参数是一个指向 struct sockaddr 类型的结构体的指针,用于指定接收数据的远程地址。dest_len
参数是 dest_addr 结构体的长度。函数返回值:
函数返回值为发送数据的字节数,如果发生错误,则返回 -1,错误码保存在 errno 变量中。
udpServer.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace Server
{
using namespace std;
static const string defaultIp = "0.0.0.0"; //TODO
static const int gnum = 1024;
enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};
class udpServer
{
public:
udpServer(const uint16_t &port, const string &ip = defaultIp)
:_port(port), _ip(ip), _sockfd(-1)
{}
void initServer()
{
// 1. 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(SOCKET_ERR);
}
cout << "socket success: " << " : " << _sockfd << endl;
// 2. 绑定port,ip(TODO)
// 未来服务器要明确的port,不能随意改变
struct sockaddr_in local; // 定义了一个变量,栈,用户
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip.c_str());
//local.sin_addr.s_addr = htonl(INADDR_ANY); // 任意地址bind,服务器的真实写法
int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if(n == -1)
{
cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
exit(BIND_ERR);
}
// UDP Server 的预备工作完成
}
void start()
{
// 服务器的本质其实就是一个死循环
char buffer[gnum];
for(;;)
{
// 读取数据
struct sockaddr_in peer;
socklen_t len = sizeof(peer); //必填
ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
// 1. 数据是什么 2. 谁发的?
if(s > 0)
{
buffer[s] = 0;
string clientip = inet_ntoa(peer.sin_addr); //1. 网络序列 2. int->点分十进制IP
uint16_t clientport = ntohs(peer.sin_port);
string message = buffer;
cout << clientip <<"[" << clientport << "]# " << message << endl;
}
}
}
~udpServer()
{
}
private:
uint16_t _port;
string _ip; // 实际上,一款网络服务器,不建议指明一个IP
int _sockfd;
// func_t _callback; //回调
};
}
udpServer.cc
#include "udpServer.hpp"
#include
using namespace std;
using namespace Server;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " local_port\n\n";
}
// ./udpServer port
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<udpServer> usvr(new udpServer(port));
usvr->initServer();
usvr->start();
return 0;
}
udpClient.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace Client
{
using namespace std;
class udpClient
{
public:
udpClient(const string &serverip, const uint16_t &serverport)
: _serverip(serverip),_serverport(serverport), _sockfd(-1), _quit(false)
{}
void initClient()
{
// 创建socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd == -1)
{
cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
exit(2);
}
cout << "socket success: " << " : " << _sockfd << endl;
// 2. client要不要bind[必须要的],client要不要显示的bind,需不需程序员自己bind?不需要
// 写服务器的是一家公司,写client是无数家公司 -- 由OS自动形成端口进行bind!-- OS在什么时候,如何bind
}
void run()
{
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(_serverip.c_str());
server.sin_port = htons(_serverport);
string message;
while(!_quit)
{
cout << "Please Enter# ";
cin >> message;
sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
}
}
~udpClient()
{
}
private:
int _sockfd;
string _serverip;
uint16_t _serverport;
bool _quit;
};
} // namespace Client
udpClient.cc
#include "udpClient.hpp"
#include
using namespace Client;
static void Usage(string proc)
{
cout << "\nUsage:\n\t" << proc << " server_ip server_port\n\n";
}
// ./udpClient server_ip server_port
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));
ucli->initClient();
ucli->run();
return 0;
}
如有错误或者不清楚的地方欢迎私信或者评论指出