目录
一.套接字
1.什么是套接字/Socket套接字
2.套接字的分类
3.Socket套接字的常见API
二.网络字节序
1.什么是网络字节序
2.网络字节序和主机字节序的转换接口
三.IP地址形式上的转换
四.客户端的套接字不由程序员bind
1.为什么客户端套接字不能由程序员bind
2.OS是在什么时候给客户端bind了ip和port
五.基于UDP的网络通信
1.传输层协议UDP的基本特性
2.基于UDP协议的C/S网络通信
1).demo代码1
2.demo代码2 - mini聊天室
六.补充扩展
1.本地环回ip地址
2.云服务器不能绑定指定IP地址
3.服务器不推荐绑定确定IP
一.套接字
1.什么是套接字/Socket套接字
套接字是通信的基石,是支持TCP/IP协议的路通信的基本操作单元。可以将套接字看作不同主机间的进程进行双间通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据(数据交换也可能穿越域的界限,但这时一定要执行某种解释程序),各种进程使用这个相同的域互相之间用Internet协议簇来进行通信
Socket(套接字)可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信的API(应用程序编程接口),也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 Socket中,该 Socket通过与网络接口卡(NIC)相连的传输介质将这段信息送到另外一台主机的 Socket中,使对方能够接收到这段信息。 Socket是由IP地址和端口结合的,提供向应用层进程传送数据包的机制
--- 摘自百度
Socket套接字属于应用层和传输层协议之间的一个抽象层, Socket套接字系列的API都是传输层来提供给我们使用的接口, 用来让我们在应用层中编写网络编程代码
在linux中, 一切皆文件, 网卡也是文件; socket套接字, 本质上, 就是一个文件描述符fd, 因为网络通信的本质也是使用文件来进行通信, 创建好套接字, 之后的网络通信, 想要发送/接收信息数据本质上都是在利用这个套接字文件来进行
2.套接字的分类
域间套接字 - 用于本主机内的进程间通信 - 基于套接字式的管道通信 - 原理类似于命名管道通信
原始套接字 - 用来编写一些工具 - 可以绕过运输层或网络层或其他层直接使用底层
网络套接字 - 用于网络通信 - 例如, socket套接字就是网络套接字
以上, 理论上应该是三套接口, 而linux"封装"了套接字类型, 将所有的接口进行了统一
其统一就体现在, sockaddr结构体类型
不管是sockaddr_in还是sockaddr_un都可以以强制类型转换的方式传给sockaddr, sockaddr依靠前两字节判断该sockaddr具体是sockaddr_in还是sockaddr_un还是其他套接字..., 补充: 为何不用void*? 因为网络接口的设计比C语言中void*更早, 所以那时还没有void*
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
以上三个头文件, 有一些网络编程中常用函数和sockaddr_in类型所需
sockaddr_in网络通信结构体类型:
总之, struct sockaddr_in内需要有:
哪种套接字(sin_family 类型: AF_INET or AF_UNIX or AF_LOCAL ...宏 --> int)
IP地址(sin_addr 类型: struct in_addr --> 内部只有一个成员s_addr 类型: in_addr_t --> uint32_t)
Port端口(sin_port 类型: in_port_t --> uint16_t)
剩下的均为填充字段(sin_zero不用管)
3.Socket套接字的常见API
#include <sys/types.h>
#include <sys/socket.h>
创建socket套接字, 即文件描述符(TCP/UDP)
本质: 创建一个指明运输层协议的文件供我们用来网络通信
int socket(int domain, int type, int protocol);
参数
1.domain: 表示套接字的域, 即套接字的类型, AF_UNIX域AF_LOCAL用于本地通信, AF_INET和AF_INET6用于网络通信(IPv4和IPv6), ...
2.type: 类型, 你想创建这个套接字的通信种类是什么, 面向流SOCK_STREAM?or面向数据报SOCK_DGRAM?or other?
3.protocol: 协议, 你想使用哪一种协议, 通常第一二个参数填好, 就代表第三个参数填好了, 如用AF_INET且SOCK_DGRAM也就代表使用传输层的UDP协议, 或AF_INET且SOCK_STREAM也就代表使用传输层的TCP协议, 通常如果采用默认传0即可
返回值: 文件描述符(在网络中就是套接字)
绑定端口号(TCP/UDP)
本质: 将用户设置的IP和port在内核中和我们当前进程及创建的socket套接字强关联
int bind(int socket, const struct sockaddr *addr, socklen_t address_len);
参数
1.socket: 我们创建的套接字
2.addr: 填充ip和port的结构体, 需要将struct sockaddr_in*强转成struct sockaddr*
3.address_len: addr结构体的长度(本质: uint32_t)
返回值: 成功返回0, 否则返回-1, 设置错误码errno
开始监听socket(TCP)
int listen(int socket, int backlog);
接收请求(TCP)
int accept(int socket, struct sockaddr *addr, socklen_t *address_len);
建立连接(TCP)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
ssize_t recv(int sockfd, void* buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)
本质: 从源ip主机, 即网络中, 接收数据
参数
sockfd: 创建并且绑定的套接字
buf: 读到buf中
len: buf大小
flags: 读取方式, 0代表阻塞式读取
src_addr: 源IP和port, 输出型参数
addrlen: 源IP和port的长度, 输入输出型参数, 输入: struct sockaddr_in的大小 输出: 实际读到的src_addr大小
注:!! src_addr与addrlen本质是要获取, 发送数据一端的ip&&port信息
返回值: 实际读取到的字节个数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
本质: 向目的ip主机, 即网络中, 发送数据
参数
sockfd: 创建并且绑定的套接字
buf: 发送数据的内容
len: 发送数据的长度
flags: 读取方式, 0代表阻塞式读取
dest_addr: 目的端的信息, ip port family...
addrlen: dest_addr的长度
二.网络字节序
1.什么是网络字节序
在计算机中, 内存存储字节有两种方式: 大端或小端
一台主机发送给另一台主机数据, 这两台机器在网络通信时并不知道对方是大端机还是小端机, 为了能够统一字节序, 便有了网络字节序这一概念
在TCP/IP协议族中, 网络字节序采用大端, 也就是不管发送什么数据, 都会按照大端(网络字节序)来发送/接收数据, 如果当前发送机是小端, 就要先将数据转成大端; 否则就忽略, 直接发送即可; 从网络中接收数据时也是同理
2.网络字节序和主机字节序的转换接口
包含头文件 <arpa/inet.h>
主机转网络
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
网络转主机
uint32_t ntohl(uint16_t netlong);
uint16_t ntohs(uint16_t netshort);
注: h - host代表主机; n - network代表网络; l - long代表长整型; s - short代表短整型
通常如果是port, 用short - uint16_t; 如果是IP, 用long - uint32_t
三.IP地址形式上的转换
为了让人更方便的看到ip地址, 通常输出/输入"点分十进制" 的ip地址, 每个.之间的数取值范围[0, 255]因为一个数只占1字节, 实际上计算机存储ip地址只需要4Byte就够了, 要是通过网络的传输的话还要把4字节ip转换为网络字节序, 这样一共有两步转换
1."点分十进制"字符串式ip --> 主机序列的4字节ip
2.主机序列的4字节ip --> 网络序列的4字节ip
以上两步可以通过同一接口进行转换
"点分十进制"字符串式ip --> 网络序列的4字节ip: in_addr_t inet_addr(const char* cp);
网络序列的4字节ip --> "点分十进制"字符串式ip: char *inet_ntoa(struct in_addr in);
四.客户端的套接字不由程序员bind
1.为什么客户端套接字不能由程序员bind
客户端client一定需要绑定ip和端口, 但是由用户的OS自动绑定的, 不能由程序员手动绑定, 因为程序员写的客户端client在未来是要交给用户, 并且让用户下载的, 程序员并不知道在用户的机器上的端口的使用情况, 由于1个端口只能绑定一个客户端, 所以这个绑定的过程要交给用户的机器OS自动进行
2.OS是在什么时候给客户端bind了ip和port
当客户端client首次发送信息给服务器的时候, 用户的OS会自动给用户客户端client绑定它的ip和port
五.基于UDP的网络通信
1.传输层协议UDP的基本特性
有连接
可靠传输
面向字节流
2.基于UDP协议的C/S网络通信
1).demo代码1
udp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <cassert>
#include <strings.h>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class udpServer
{
public:
udpServer(const uint16_t port, const std::string ip = "") : _port(port), _ip(ip)
{
}
void Init()
{
// 1.创建Socket套接字
_socket = socket(AF_INET, SOCK_DGRAM, 0);
assert(_socket != -1);
std::cout << "创建Socket套接字成功" << std::endl;
// 2.绑定Socket套接字 -- ip和port
struct sockaddr_in sv;
bzero(&sv, sizeof(sv));
sv.sin_family = AF_INET;
sv.sin_port = htons(_port);
sv.sin_addr.s_addr = _ip == "" ? INADDR_ANY : inet_addr(_ip.c_str());
socklen_t len = sizeof(sv);
int ret = bind(_socket, (struct sockaddr *)&sv, len);
assert(ret != -1);
std::cout << "绑定Socket套接字成功" << std::endl;
}
void Start()
{
while (true)
{
// 3.开始通信
char buffer[1024];
bzero(buffer, sizeof(buffer));
// 3.1接收数据
struct sockaddr_in cli;
bzero(&cli, sizeof(cli));
socklen_t len = sizeof(cli);
ssize_t n = recvfrom(_socket, buffer, sizeof(buffer), 0, (struct sockaddr *)&cli, &len);
if (n > 0)
{
buffer[n] = '\0';
// 3.2输出读取数据
std::string cli_ip = inet_ntoa(cli.sin_addr);
uint16_t cli_port = ntohs(cli.sin_port);
printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
// 4.原路发回
sendto(_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&cli, len);
}
else
{
std::cout << "本次未读到数据" << std::endl;
}
}
}
~udpServer()
{
close(_socket);
}
private:
uint16_t _port; // 端口
std::string _ip; // ip
int _socket = -1; // Socket套接字
};
udp_server.cc
#include "udp_server.hpp"
#include <memory>
static inline void Usage(char *&proc)
{
std::cout << "correct way to use: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(-1);
}
uint16_t server_port = atoi(argv[1]);
std::unique_ptr<udpServer> server(new udpServer(server_port));
// 初始化服务器
server->Init();
// 启动服务器
server->Start();
return 0;
}
client.cc
#include "udp_server.hpp"
#include <memory>
static inline void Usage(char *&proc)
{
std::cout << "correct way to use: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(-1);
}
uint16_t server_port = atoi(argv[1]);
std::unique_ptr<udpServer> server(new udpServer(server_port));
// 初始化服务器
server->Init();
// 启动服务器
server->Start();
return 0;
}
2.demo代码2 - mini聊天室
udp_server.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <cassert>
#include <strings.h>
#include <unistd.h>
#include <cstring>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class udpServer
{
public:
udpServer(const uint16_t port, const std::string ip = "") : _port(port), _ip(ip)
{
}
void Init()
{
// 1.创建Socket套接字
_socket = socket(AF_INET, SOCK_DGRAM, 0);
assert(_socket != -1);
std::cout << "创建Socket套接字成功" << std::endl;
// 2.绑定Socket套接字 -- ip和port
struct sockaddr_in sv;
bzero(&sv, sizeof(sv));
sv.sin_family = AF_INET;
sv.sin_port = htons(_port);
sv.sin_addr.s_addr = _ip == "" ? INADDR_ANY : inet_addr(_ip.c_str());
socklen_t len = sizeof(sv);
int ret = bind(_socket, (struct sockaddr *)&sv, len);
assert(ret != -1);
std::cout << "绑定Socket套接字成功" << std::endl;
}
void Start()
{
while (true)
{
// 3.开始通信
char buffer[1024];
bzero(buffer, sizeof(buffer));
// 3.1接收数据
struct sockaddr_in cli;
bzero(&cli, sizeof(cli));
socklen_t len = sizeof(cli);
ssize_t n = recvfrom(_socket, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&cli, &len);
char key[64];
bzero(key, sizeof(key));
if (n > 0)
{
buffer[n] = '\0';
// 3.2输出读取数据
std::string cli_ip = inet_ntoa(cli.sin_addr);
uint16_t cli_port = ntohs(cli.sin_port);
printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer); // 给服务器打印
// 注册发送数据的用户信息
snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port);
_usrmessage[key] = cli;
}
// 向所有注册过的用户广播信息
std::string message;
message += key;
message += "#";
message += buffer;
message += "\n";
for (auto &e : _usrmessage)
{
std::cout << "广播............" << std::endl;
// 4.原路发回
sendto(_socket, message.c_str(), message.size(), 0, (struct sockaddr *)&(e.second), sizeof(e.second));
}
}
}
~udpServer()
{
close(_socket);
}
private:
uint16_t _port; // 端口
std::string _ip; // ip
int _socket = -1; // Socket套接字
std::unordered_map<std::string, struct sockaddr_in> _usrmessage;
};
udp_server.cc
#include "udp_server.hpp"
#include <memory>
static inline void Usage(char *&proc)
{
std::cout << "correct way to use: " << proc << " port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(-1);
}
uint16_t server_port = atoi(argv[1]);
std::unique_ptr<udpServer> server(new udpServer(server_port));
// 初始化服务器
server->Init();
// 启动服务器
server->Start();
return 0;
}
udp_client.hpp
#pragma once
#include <iostream>
#include <cstdlib>
#include <strings.h>
#include <memory>
#include <unistd.h>
#include <thread>
#include <cstdio>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
class udpClient
{
private:
static void Send(struct sockaddr_in sendUsr, int sock)
{
while (true)
{
// 2.发送数据
std::string sendmessage;
std::cout << "请输入: ";
std::getline(std::cin, sendmessage);
sendto(sock, sendmessage.c_str(), sendmessage.size(), 0, (struct sockaddr *)&sendUsr, sizeof(sendUsr));
if (sendmessage == "quit")
{
std::cout << "the clientSend is closed" << std::endl;
break;
}
}
}
static void Recv(int sock)
{
std::string namedPath = "./JOKER.txt";
umask(0);
mkfifo(namedPath.c_str(), 0666);
int fd = open(namedPath.c_str(), O_WRONLY | O_APPEND);
while (true)
{
// 3.接收server echo
char buffer[1024];
bzero(buffer, sizeof(buffer));
ssize_t n = recv(sock, buffer, sizeof(buffer), 0);
if (n > 0)
{
buffer[n] = '\0';
//std::cout << "Server Echo# " << buffer << std::endl;
write(fd, buffer, sizeof(buffer));
if (buffer == "quit")
{
std::cout << "the clientRecv is closed" << std::endl;
break;
}
}
}
close(fd);
}
public:
udpClient(std::string sip, uint16_t sport) : _sv_ip(sip), _sv_port(sport), _socket(-1)
{
}
void Init()
{
// 1.创建套接字
_socket = socket(AF_INET, SOCK_DGRAM, 0);
// 客户端由客户机OS自动绑定
}
void Start()
{
struct sockaddr_in sendUsr;
bzero(&sendUsr, sizeof(sendUsr));
sendUsr.sin_family = AF_INET;
sendUsr.sin_addr.s_addr = inet_addr(_sv_ip.c_str());
sendUsr.sin_port = htons(_sv_port);
std::thread t_send(Send, sendUsr, _socket);
std::thread t_recv(Recv, _socket);
t_send.join();
t_recv.join();
}
~udpClient()
{
close(_socket);
}
private:
std::string _sv_ip;
uint16_t _sv_port;
int _socket;
};
udp_client.cc
#include "udp_client.hpp"
static inline void Usage(char *&proc)
{
std::cout << "correct way to use: " << proc << " ip port" << std::endl;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(-1);
}
std::string sv_ip = argv[1];
uint16_t sv_port = atoi(argv[2]);
std::unique_ptr<udpClient> client(new udpClient(sv_ip, sv_port));
client->Init();
client->Start();
return 0;
}
六.补充扩展
1.本地环回ip地址
ip地址: 127.0.0.1 --> 本地环回
client和server发送数据指在本地协议栈中进行数据流动, 不会把我们的数据发送到网络中
只是把TCP/IP五层协议栈走一遍, 通常用来做本地网络服务器的测试
2.云服务器不能绑定指定IP地址
云服务器无法绑定公网IP地址
在云服务器中, 比如腾讯云, 他给我们提供的IP地址全部都是虚拟出来的, 不允许绑定这个确定的ip地址, 实际使用的不是这个IP地址, 所以我们无法在服务器进程上直接绑定这个地址
但其他人是可以通过这个虚拟出来的ip向云服务器互相网络通信的, 因为可以通过服务器进程绑定任意ip地址的方式来达到
3.服务器不推荐绑定确定IP
推荐服务器绑定"任意IP" -- 使用INADDR_ANY宏
INADDR_ANY宏, 本质是(in_addr_t)0x0000 0000 -- 让服务器在工作过程中可以从任意IP中获取数据 -- 即可以理解为有多少张网卡有多少IP都可以绑定上
因为服务器也许不止一张网卡, 也许有很多张网卡, 如果绑定了具体IP地址, 则服务器只能收到发送到这个IP地址的消息, 而如果使用任意IP的绑定方法, 则服务器就能收到发送到很多个IP地址的消息, 比如该服务器有3张网卡, 则客户端有可能分别向这三张网卡的IP发送数据, 服务器就都能收到了
当在机器上查看一个服务器进程或客户端进程的IP地址为0.0.0.0就说明为任意IP地址绑定