目录
(4)主机改变所在网络时需要修改IP地址,不可修改MAC地址
(3)把字符串风格的IP地址转为4字节地址 inet_addr ,4字节转字符串 inet_ntoa
(4)本地通信:127.0.0.11——本地环回—代表本主机
④chmod +x udpClient 将程序转为可执行程序
6.windows做客户端,linux做服务器的联网通信 步骤(udp套接字)
软件是可以分层的,为什么要分层?
1.软件在分层的同时,也把问题归类的
2.分层的本质:软件上解耦
3.便于工程师进行软件维护
网络本身的代码,就是层状结构!
层状结构下的网络协议,我们认为,同层协议 都可以认为自已在和对方直接通信,忽略底层细节同层之间一定都要有自己的协议。
在下面这个例子中, 我们的协议只有两层(汉语协议和电话机协议); 但是实际的网络通信会更加复杂, 需要分更多的层次。分层最大的好处在于 "封装",面向对象例子。
(1)体系结构直接决定, 数据包在主机内进行流动的时候,一定是要进行自顶向下(封包)或者自底向上(解包)进行流动的。以前的所有的IO都是这样的。
(2)tcp/ip协议和操作系统之间的关系是:操作系统内部,有一个模块,就叫做tcp/ip协议(传输层和网络层),网络协议栈是隶属于OS的。
(3)同层协议都认为自已在和对方直接通信——所以每一层都要有自己的协议
(4)重谈协议——计算机的视角,如何看待协议:① 体现在代码逻辑上 ② 体现在数据上
以寄快递为例:你和卖家沟通好,买一个鼠标,实际上快递员给你的是一个包裹,里面有鼠标,
实际上多给了我一些东西,多了一张快递单,快递单是一块数据=>快递公司和快递点,快递小哥之间的协议。为了维护协议,一定要在被传输的数据上,新增其他数据(协议数据)
有效载荷的分用过程:数据包添加报头的时候,也要考虑未来解包的时候,将自己的有效载荷交付给上层的哪一个协议!
下图为数据分用的过程:
两个结论:(大部分协议的公共属性)
1.一般而言,任何报头属性里面,一定要存在的一些字段支持,我们进行封装和解包,即:报头中一定要存着用于 区分报头和有效载荷 的数据
2.一般而言,任何报头属性里面,一定要存在的一些字段支持我们进行分用。即:报头中一定要存着用于 得知报文的有效载荷要给上层哪个协议 的数据
路由器可看做一个主机同时横跨了两个局域网
所有的IP向上的协议,发送和接受主机看到的数据是一模一样的
网络 -> IP网络,IP协议屏蔽了底层网络的差异! ! !
数据“你好”从客户发出,不断封装,到以太网驱动程序完成最后封装,再通过以太网传输给路由器下的以太网驱动程序,路由器下的以太网驱动程序解包数据,传给路由器,路由器发现这个数据是要传给IPB的,再通过路由器下的以太网驱动程序封装,通过令牌环传输给目标主机所在网络,自底向上解包传输
网络传输数据的本质就是数据不断的封装和解包。路由器可看做一个主机同时横跨了两个局域网
所有的数据,必须在”网线”上跑!
如果两台主机,处于同一个局城网。这两台主机可以直接通信——以太网,一种局域网的标准(以太——物理学界太空中不存在的物质叫以太,为了致敬命名以太网)以太网:站在系统的角度 就是 两台主机之间的临界资源。
1.每一台主机都要有唯一的标识:该主机对应的MAC地址!
2.任何一台主机,在任何时刻,都可以随时发消息——碰撞域——无法准确的听到对应的消息——识别发生了碰撞(碰撞检测)——碰撞避免——等不碰撞了过一会儿再发消息
目的IP地址:通信主机目的主机
两主机可以在同一个局域网也可以不在。
我们在网络通信的时候,不止是让两台主机通信。实际上,在进行通信的时候,不仅仅要考虑两台主机间互相交互数据。本质上讲,进行数据交互的时候是用户和用户在进行交互。用户的身份,通常是用程序体现的。程序一定是在运行中——进程!
主机间在通信的本质是:在各自的主机上的两个进程在互相交互数据!
IP地址可以完成主机和主机的通信,而主机上各自的通信进程,才是发送和接受数据的一方
IP :确保主机的唯一性
端口号(port):确保该主机上某一个进程的唯一性(则一个进程只能占用一个端口号)
IP:PORT = 标识互联网中唯一的一个进程!——>这两个合起来叫 socket(套接字)(翻译是插座)
网络通信的本质:就是进程间通信! ! !
源IP:源端口, 目的IP:目的端口——两个socket对
uint32_t htonl (uint32_ t hostlong); ——htonl(host to net 主机转网络)
man socket,man htons,man inet_ addr查看所有头文件
#include
#include
#include
#include
man 2 socket
int socket(int domain, int type, int protocol);
domain:socket网络通信的域——网络通信 (AF_INET /PF_INET )(或 本地通信 (AF_UNIX))。现在只用AF_INET 网络通信(有的地方把AF_INET写成PF_INET也是正确的)
type:套接字类型——决定了我们通信的时候对应的报文类型(流式 / 用户数据报式)
流式套接:SOCK_STREAM ——用于TCP协议
用户数据报式套接:SOCK_DGRAM ——用于UDP协议
protocol:协议类型——网络应用中设置为 0。(因为AF_INET+SOCK_STREAM—默认是TCP套接字;AF_INET+SOCK_DGRAM—默认是UDP套接字)
返回值:成功返回文件描述符(套接字描述符),错误返回-1并设置错误码(套接字类型本质就是文件描述符)
man 2 bind
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
sockfd:套接字这个文件描述符。addr:传入我们自己创建的信息 struct sockaddr_in local 的地址,然后把它强转成struct sockaddr类型结构体,内部会自动识别是什么类型的套接字做绑定。addrlen:sockaddr类型结构体 的大小
返回值:成功返回0,失败返回-1
例如:if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
in_addr_t inet_addr(const char *cp); 把字符串风格的IP地址 cp 转为4字节地址并返回。inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n 主机字节序转网络字节序(使用后就不用再调用htonl了)注意:这类函数在转变IP风格时都会自动进行主机字节序和网络字节序之间的转换。
返回值:成功返回IP对应的网络字节序的数;失败返回INADDR_NONE;
in_addr_t就是4字节类型
char *inet_ntoa(struct in_addr in); 把4字节IP地址转为字符串风格的IP地址并返回。
例子: std::string peerIp = inet_ntoa(peer.sin_addr); //拿到了对方的IP
inet_ntoa不是线程安全的函数
因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果。
man recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
从特定套接字 sockfd中读取数据到缓冲区buf中,buf大小为len,flags设为0——阻塞式读取
src_addr:(输出型参数)当服务器读取客户端发送的消息时——哪个客户端给你发的消息,就把这个客户端套接字信息存入src_addr中。(src_addr的类型是套接字类型指针struct sockaddr*,传入的网络套接字类型struct sockaddr_in*需要强转成此类型指针 struct sockaddr*。)
addrlen:(输入输出型参数)客户端这个缓冲区大小。(socklen_t就是unsigned int)
返回值:返回读到的字节数,错误就返回-1错误码被设置
当客户端使用recvfrom读取服务器返回发送的消息时——src_addr和addrlen没意义,但是还是要定义一个套接字类型结构体添上占位
- void *recverAndPrint(void *args)
- {
- while (true)
- {
- int sockfd = *(int *)args;
- char buffer[1024];
- struct sockaddr_in temp;
- socklen_t len = sizeof(temp);
- ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0,
- (struct sockaddr *)&temp, &len);
- if (s > 0)
- {
- buffer[s] = 0;
- std::cout << "server echo# " << buffer << std::endl;
- }
- }
- }
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
通过客户端的指定套接字sockfd,发送buf中的数据,buf的大小是len,flags=0 默认阻塞式发送,
dest_addr:(输入型参数)向哪个主机发消息,套接字类型指针struct sockaddr*,传入的网络套接字类型struct sockaddr*需要强转成此类型指针 struct sockaddr*。
addrlen:(输入型参数)主机这个缓冲区大小。(socklen t就是unsigned int)
返回值:返回读到的字节数,错误就返回-1错误码被设置
(首次调用sendto函数的时候,我们的client会自动bind自己的ip和port)
在C/C++中会遇到需要定义使用可变参数的函数,例如printf就是,他的格式就是int printf(const char *format,...),对于这样类型的函数,他的实现实际上就是从format格式的指针指向的空间中读取可变参数的类型,然后根据可变参数的首地址读取相应的可变参数值
va_list ap; va_start 就是char* 指针类型。
void va_start(va_list ap, last); va_start(ap, format);——获取可变参数的首地址并赋值给ap
type va_arg(va_list ap, type); ——提取ap,根据type参数类型获取实参值返回
void va_end(va_list ap); ——将 ap 置空,即将可变参数指针归NULL
void va_copy(va_list dest, va_list src);
int vsnprintf(char *str, size_t size, const char *format, va_list ap); 通过读取format得到可变参数的类型,将用户格式化的可变参数内容写入数组str中
str:把格式化内容写进str这个数组中。size:被写入空间的大小 sizeof(str)-1(不包含'\0')。format:存储 可变参数的类型 的空间。ap:可变部分
- #pragma once
-
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define DEBUG 0
- #define NOTICE 1
- #define WARINING 2
- #define FATAL 3
-
- const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};
-
- // logMessage(DEBUG, "%d", 10);
- void logMessage(int level, const char *format, ...) level日志等级
- {
- assert(level >= DEBUG);
- assert(level <= FATAL);
-
- char *name = getenv("USER");
-
- char logInfo[1024];
- va_list ap; // ap -> char*
- va_start(ap, format);//用离可变参数format最近的参数初始化ap
-
- vsnprintf(logInfo, sizeof(logInfo)-1, format, ap);
-
- va_end(ap); // ap = NULL
-
-
- FILE *out = (level == FATAL) ? stderr:stdout;
-
- fprintf(out, "%s | %u | %s | %s\n", \
- log_level[level], \
- (unsigned int)time(nullptr),\
- name == nullptr ? "unknow":name,\
- logInfo);
-
- // char *s = format;
- // while(s){
- // case '%':
- // if(*(s+1) == 'd') int x = va_arg(ap, int);
- // break;
- // }
- }
易错:1. port_ 端口号是一个 2字节16位的整数,主机转网络要用htos,不能用htol(这个错误找了一天呐~)server.sin_port=htons(server_port);
htol 是转换四字节的,如果你传入一个两字节的数据,它就会自动进行补位,补位前面部分都是零,那这时候经过htol置换之后,前16位就变成零了,相当于你的程序跑去绑定零端口去了,就会绑定失败。
#define INADDR_ANY ((in_addr_t) 0x00000000)
local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
①INADDR_ANY(这个宏的值就是0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法(解释:一般服务器只有一个IP,会自动bind这个IP;如果服务器有多个IP,会自动bind这个服务器的所有的IP——因为如果有两个IP:IP1和IP2,只bind一个IP1,那么只有传给IP1的报文会交给程序,IP2就不会提交报文)
云服务器有一些特殊情况:禁止你bind云服务器上的任何确定IP, 所以这里只能使用INADDR_ANY,如果你是虚拟机就可以bind自己虚拟机的IP,用ifconfig查看IP。
注意:这里inet_addr(ip_.c_str()) 当ip_是"0"时 等价于INADDR_ANY,INADDR_ANY 这个宏的值就是0,0是字符串风格还是网络风格无所谓,并且inet_addr 还会自动给我们进行 h—>n 主机字节序转网络字节序,即 inet_addr(0)=inet_addr(INADDR_ANY)=htonl(INADDR_ANY) 作用是一样的
in_addr_t inet_addr(const char *cp); 把字符串风格的IP地址 cp 转为4字节地址并返回。inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用。因为IP地址也是会发给对方的,所以除了做转化,inet_addr 还会自动给我们进行 h—>n 主机字节序转网络字节序(使用后就不用再调用htonl了)(INADDR_ANY 是0,所以h—>n转不转都行)
bzero(&local,sizeof(1ocal)); ——bzero函数将从s开始的区域的前n个字节设置为0(字节包含'\0'). 也可以用memset代替
客户端发送消息到本地的网络协议栈,但是不发送到网络,仅通过本地网络协议栈向上交付给另一个进程的缓冲区中。
init中创建套接字不能加int,否则sock就是局部变量了
服务器:创建套接字,填充信息,bind绑定,recvfrom等待接收消息,checkOnlineUser 添加在线用户,messageRoute 消息路由
客户端:创建套接字,填充服务器的信息,创建线程去recvfrom等待路由消息,主线程发消息给服务器
- .PHONY:all
- all:udpClient udpServer
-
- udpClient: udpClient.cc
- g++ -o $@ $^ -std=c++11 -lpthread
- udpServer:udpServer.cc
- g++ -o $@ $^ -std=c++11
-
- .PHONY:clean
- clean:
- rm -f udpClient udpServer
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- struct sockaddr_in server;
-
- static void Usage(std::string name)
- {
- std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
- }
-
- void *recverAndPrint(void *args)
- {
- while (true)
- {
- int sockfd = *(int *)args;
- char buffer[1024];
- struct sockaddr_in temp;
- socklen_t len = sizeof(temp);//这个temp套接字结构体在这里不接收任何信息,只占位参数
- ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
- if (s > 0)
- {
- buffer[s] = 0;
- std::cout << "server echo# " << buffer << std::endl;
- }
- }
- }
-
- // ./udpClient server_ip server_port
- // 如果一个客户端要连接server必须知道server对应的ip和port
- int main(int argc, char *argv[])
- {
- if (argc != 3)
- {
- Usage(argv[0]);
- exit(1);
- }
- // 1. 根据命令行,设置要访问的服务器IP
- std::string server_ip = argv[1];
- uint16_t server_port = atoi(argv[2]);
-
- // 2. 创建客户端
- // 2.1 创建socket
- int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
- assert(sockfd > 0);
-
- // 2.2 client 需不需要bind??? 需要bind,但是不需要用户自己bind,而是os自动给你bind
- // 所谓的"不需要",指的是: 不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!
- (OS随机申请生成一个进程并让这个进程去绑定运行客户端)
- // 如果我非要自己bind呢?可以!严重不推荐!
- // 所有的客户端软件 <-> 服务器 通信的时候,必须得有client[ip:port]<->server[ip:port]
- // 为什么不需要用户自己bind端口信息呢??client很多,不能给客户端bind指定的port,port
- 可能被别的client使用了,你的client就无法启动了
- // 那么server凭什么要bind呢??server提供的服务,必须被所有人知道!server不能随便改变!
- server的端口号必须确定,但是客户端的端口号是多少不重要,因为没人连你的客户端,是你
- 连别人的服务器
-
- // 2.2 填写服务器对应的信息
-
- bzero(&server, sizeof server);
- server.sin_family = AF_INET;
- server.sin_port = htons(server_port);
- server.sin_addr.s_addr = inet_addr(server_ip.c_str());
-
- pthread_t t;
- pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);
- // 3. 通讯过程
- std::string buffer;
- while (true)
- {
- std::cerr << "Please Enter# ";
- std::getline(std::cin, buffer);
- // 发送消息给server
- sendto(sockfd, buffer.c_str(), buffer.size(), 0,
- (const struct sockaddr *)&server, sizeof(server)); // 首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
- }
-
- close(sockfd);
-
- return 0;
- }
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- #include "Log.hpp"
-
- static void Usage(const std::string porc)
- {
- std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
- }
-
- /// @brief 我们想写一个简单的udpSever
- /// 云服务器有一些特殊情况:
- /// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意
- class UdpServer
- {
- public:
- UdpServer(int port, std::string ip = "") : port_((uint16_t)port), ip_(ip), sockfd_(-1)
- {
- }
- ~UdpServer()
- {
- }
-
- public:
- void init()
- {
- // 1. 创建socket套接字
- sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 就是打开了一个文件
- if (sockfd_ < 0)
- {
- logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);
- exit(1);
- }
- logMessage(DEBUG, "socket create success: %d", sockfd_);
- // 2. 绑定网络信息,指明ip+port
- // 2.1 先填充基本信息到 struct sockaddr_in
- struct sockaddr_in local; // local在哪里开辟的空间? 用户栈 -> 临时变量 -> 写入内核中
- bzero(&local, sizeof(local)); // 可以用memset代替
- // 填充协议家族,域,选择是网络通信还是本地通信
- local.sin_family = AF_INET; sin_family就是开头的16位地址类型:AF_ INET
- // 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中
- local.sin_port = htons(port_); port_类内成员是本地序列,要用htons转网络序列
- // 服务器都必须具有IP地址,42.192.83.143 "xx.yy.zz.aaa" ,字符串风格点分十进制 -> 4字节IP
- -> uint32_t ip(每个数字是0~255,8bit)
- // INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法
- // inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n
- local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
- // 2.2 bind 网络信息
- if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
- {
- logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);
- exit(2);
- }
- logMessage(DEBUG, "socket bind success: %d", sockfd_);
- // done
- }
-
- void start()
- {
- // 服务器设计的时候,服务器都是死循环
- char inbuffer[1024]; //将来读取到的数据,都放在这里
- char outbuffer[1024]; //将来发送的数据,都放在这里
- while (true)
- {
- struct sockaddr_in peer; //输出型参数
- socklen_t len = sizeof(peer); //输入输出型参数
-
- // demo2
- // UDP无连接的
- // 对方给你发了消息,你想不想给对方回消息?要的!后面的两个参数是输出型参数
- ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0,
- (struct sockaddr *)&peer, &len);
- if (s > 0)
- {
- //'\0'的值就是0,'0'的值是48,这里是存ASCII为0的'\0'
- inbuffer[s] = 0; //当做字符串
- }
- else if (s == -1)
- {
- logMessage(WARINING, "recvfrom: %s:%d", strerror(errno), sockfd_);
- continue;
- }
- // 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]
- std::string peerIp = inet_ntoa(peer.sin_addr); //拿到了对方的IP,因为inet_ntoa这个函数参数类型
- 就是in_addr而不是in_addr_t,所以参数填peer.sin_addr而不是peer.sin_addr.s_addr
- uint32_t peerPort = ntohs(peer.sin_port); // 拿到了对方的port
-
- checkOnlineUser(peerIp, peerPort, peer); //如果存在,什么都不做,如果不存在,就添加
-
- // 打印出来客户端给服务器发送过来的消息
- logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);
-
- // for(int i = 0; i < strlen(inbuffer); i++)
- // {
- // if(isalpha(inbuffer[i]) && islower(inbuffer[i])) outbuffer[i] = toupper(inbuffer[i]);
- // else outbuffer[i] = toupper(inbuffer[i]);
- // }
- messageRoute(peerIp, peerPort,inbuffer); //消息路由
-
- // 线程池!
-
- // sendto(sockfd_, outbuffer, strlen(outbuffer), 0, (struct sockaddr*)&peer, len);
-
- // demo1
- // logMessage(NOTICE, "server 提供 service 中....");
- // sleep(1);
- }
- }
-
- void checkOnlineUser(std::string &ip, uint32_t port, struct sockaddr_in &peer)
- {
- std::string key = ip;
- key += ":";
- key += std::to_string(port);
- auto iter = users.find(key);
- if(iter == users.end())
- {
- users.insert({key, peer});
- }
- else
- {
- // iter->first, iter->second->
- // do nothing
- }
- }
-
- void messageRoute(std::string ip, uint32_t port, std::string info)
- {
-
- std::string message = "[";
- message += ip;
- message += ":";
- message += std::to_string(port);
- message += "]# ";
- message += info;
-
- for(auto &user : users)
- {
- sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));
- }
- }
-
- private:
- // 服务器必须得有端口号信息
- uint16_t port_;
- // 服务器必须得有ip地址
- std::string ip_;
- // 服务器的socket fd信息
- int sockfd_;
- // onlineuser
- std::unordered_map
struct sockaddr_in> users; - };
-
- // struct client{
- // struct sockaddr_in peer;
- // uint64_t when; //peer如果在when之前没有再给我发消息,我就删除这用户
- // }
-
- // ./udpServer port [ip]
- int main(int argc, char *argv[])
- {
- if (argc != 2 && argc != 3) //反面:argc == 2 || argc == 3
- {
- Usage(argv[0]);
- exit(3);
- }
- uint16_t port = atoi(argv[1]);
- std::string ip;
- if (argc == 3)
- {
- ip = argv[2];
- }
-
- UdpServer svr(port, ip);
- svr.init();
- svr.start();
-
- return 0;
- }
-
- // struct ip
- // {
- // uint32_t part1:8;
- // uint32_t part2:8;
- // uint32_t part3:8;
- // uint32_t part4:8;
- // }
- // struct ip ip_;
- // ip_.part1 = s.substr();
相当于发布软件
打开服务器 ./udpServer,此时服务器阻塞等待有人发消息。各个客户端:./udpClient +服务器的公网IP+8080(端口号),就可以发消息通信了
windows上的客户端代码框架
- #pragma comment(lib, "ws2_32.lib") // 需要包含的链接库
- #include
- #include
- #include
// windows socket 2.2版本 -
- int main()
- {
- WSADATA wsaData; // 用作初始化套接字
- WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化启动信息(初始套接字)
-
- 客户端的创建套接字,填充服务器的信息,sendto通信
-
- closesocket(SendingSocket); // 释放套接字
- WSACleanup(); // 清空启动信息
- system("pause");
- return 0;
- }
全代码
发送这里的可执行程序就可以通信了