🏆个人主页:企鹅不叫的博客
🌈专栏
⭐️ 博主码云gitee链接:代码仓库地址
⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!
💙系列文章💙
【网络编程】第一章 网络基础(协议+OSI+TCPIP+网络传输的流程+IP地址+MAC地址)
文章目录
💎一、概念介绍
🏆1.源IP地址和目的IP地址
IP地址是用来标识网络中不同主机的地址。
- 源IP地址: 发送方主机的IP地址,保证响应主机“往哪放”
- 目的IP地址: 接收方主机的IP地址,保证发送方主机“往哪发”
🏆2.源MAC地址和目的MAC地址
MAC地址是网卡决定的,是固定的。
- 目标MAC地址就是对方的MAC地址;
- 源MAC地址就是你自己的MAC地址。
🏆3.端口号
端口号(port)的作用实际就是标识一台主机上的一个进程。
- 端口号是传输层协议的内容。
- 端口号是一个2字节16位的整数。
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
- 一个端口号只能被一个进程占用。
- bind端口号时,用1024之后的位置
IP地址能够唯一标识公网内的一台主机,而端口号能够唯一标识一台主机上的一个进程,因此用IP地址+端口号就能够唯一标识网络上的某一台主机的某一个进程
- 源端口号: 发送方主机的服务进程绑定的端口号,保证接收方能够找到对应的服务
- 目的端口号: 接收方主机的服务进程绑定的端口号,保证发送方能够找到对应的服务
socket(插座):一个程序创建一个socket发送数据,另一个程序创建一个socket来接收数据,两个socket配对后就可以传输数据了
socket通信的本质: 跨网络的进程间通信。从上面可以看出,网络通信就是两台主机上的进程在进行通信。
🏆4.端口号PORT VS 进程PID
二者都是用来唯一标识某一个进程。
- 进程ID(PID)是用来标识系统内所有进程的唯一性的,它是属于系统级的概念
- 而端口号(port)是用来标识需要对外进行网络数据请求的进程的唯一性的,它是属于网络的概念
- 二者是不同层面表示进程唯一性的机制。
🏆5.TCP协议和UDP协议
两个协议都是传输层协议
TCP(Transmission Control Protocol)协议: 传输控制协议,要校队发出的数据,可靠协议,复杂
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP(User Datagram Protocol)协议: 用户数据报协议,发送完数据后就不管了,不可靠协议,简单
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
🏆6.网络字节序
- 大端字节序: 高位存放在低地址,低位存放在高地址
- 小端字节序: 低位存放在低地址,高位存放在高地址
网络数据流同样有大端和小端之分,发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 先发出的数据是低地址,后发出的数据是高地址
- TCP/IP协议规定,网络数据流采用
大端字节序
,不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据所以如果发送的主机是小端机,就需要把要发送的数据先转为大端,再进行发送,如果是大端,就可以直接进行发送
调用以下库函数实现网络字节序和主机字节序之间的转换
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t hostshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort);
- h代表的是host,n代表的是network,s代表的是16位的短整型,l代表的是32位长整形
- 如果主机是小端字节序,函数会对参数进行处理,进行大小端转换
- 如果主机是大端字节序,函数不会对这些参数处理,直接返回
🏆7.头文件
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
💎二、socket常见API
🏆1.socket的API
创建套接字socket :(TCP/UDP,客户端+服务器)
int socket(int domain, int type, int protocol);
绑定端口号:(TCP/UDP,服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
监听套接字socket :(TCP,服务器)
int listen(int sockfd, int backlog);
接收请求:(TCP,服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
建立连接:(TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 记忆:
服务器端口,socket -> bind -> listen -> accept -> send/recv -> close
服务器端口, “拿起电话 -> 设置号码 -> 等待来电 -> 接听 -> 交谈 -> 挂断”
客户端口,socket -> connect -> send/recv -> close
客户端口,“拿起电话 -> 拨号 -> 交谈 -> 挂断”
🏆2.sockaddr结构
- sockaddr_in用来进行
网络通信
,sockaddr_un结构体用来进行本地通信
- sockaddr_in结构体存储了协议家族,端口号,IP等信息,网络通信时可以通过这个结构体把自己的信息发送给对方,也可以通过这个结构体获取远端的这些信息
- 可以看出,这三个结构体的前16位是一样的,代表的是协议家族,可以根据这个参数判断需要进行哪种通信(本地和网络)
- 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; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针为参数
struct sockaddr是通用套接字结构,不直接使用,通用信封不直接使用
//定义
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址数据
};
struct sockaddr_in专门用于IPv4地址,先填充 sockaddr_in,然后将其强制转换为 sockaddr 使用
//定义
struct sockaddr_in {
sa_family_t sin_family; // 地址族 (必须是 AF_INET)
in_port_t sin_port; // 端口号 (网络字节序)
struct in_addr sin_addr; // IPv4地址
char sin_zero[8];// 填充以保持与sockaddr同样大小
};
struct in_addr {
uint32_t s_addr; // IPv4地址 (网络字节序)
};
//使用
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
💎三、UDP协议程序
🏆1.服务端
把服务器封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务器需要做的第一件事就是创建套接字。
下面是框架:
class UdpServer { public: UdpServer(int port, string ip) :_sockfd(-1) ,_port(port) ,_ip(ip) {}; void InitServer(){}//初始化 void start(){}//启动 ~UdpServer() { if (_sockfd >= 0){ close(_sockfd); } }; private: int _sockfd; //文件描述符 int _port; //端口号 string _ip; //IP地址 };
创建套接字
协议家族就是
AF_INET
对应网络通信,服务类型就是SOCK_DGRAM
对应UDP服务器,第三个参数设置为0即可void InitServer(){ //创建套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0){ //创建套接字失败 cout << "socket error" << endl; exit(1); } cout << "socket create success, sockfd: " << _sockfd << endl; }
socket
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
功能:创建套接字
参数:
- domain:通信范围,你想在哪里使用这个产品,本地(AF_UNIX)、国内(AF_INET 用于IPv4)、国际(AF_INET6 用于IPv6)
- type:通讯类型,什么类型产品,可以选择
SOCK_DGRAM
(数据报,UDP)和SOCK_STREAM
(流式服务,TCP)- protocol:具体协议,这里填写0,根据前面的参数自动推导需要那种类型
返回值: 成功返回一个文件描述符,失败返回-1
- socket函数属于应用层类型的接口
- socket函数是被进程所调用
- 调用socket函数创建套接字时,在内核层面上就形成了一个对应的
struct file
结构体,同时将3号文件描述符作为返回值返回给用户
服务器绑定
将创建的套接字和网络绑定起来,将IP地址和端口号告诉对应的网络文件,此时就可以改变网络文件当中文件操作函数的指向,所以首先要将IP地址和端口号初始化
void InitServer(){ //填充网络通信相关信息 struct sockaddr_in local; memset(&local, '\0', sizeof(local));//使用前先初始化结构体 local.sin_family = AF_INET;//填充协议家族,选择本地还是网络 local.sin_port = htons(_port); // 将主机序列的端口号转为网络序列的端口号,由于会通过网络发送,所以要从本地序列转化为网络序列 //local.sin_addr.s_addr = inet_addr(_ip.c_str());// 直向特定的IP绑定 local.sin_addr.s_addr = INADDR_ANY;// 取消单个ip绑定,可以接受来自任意client的请求,从任意ip获取数据,强烈推荐 //绑定 if (bind(_sockfd, (struct sockaddr*)&local, sizeof(local)) == -1){ cout << "bind fail" << endl; exit(2); } cout << "bind port success, port: " << _port << endl; }
定之前我们需要先定义一个
struct sockaddr_in
结构,将对应的网络属性信息填充到该结构当中,包括协议家族、端口号、IP地址注意:
- 因为数据是要发送到网络中,所以要将主机序列的端口号转为网络序列的端口号
- 络当中传输的是整数IP,我们需要调用inet_addr函数将字符串IP转换成整数IP,然后再将转换后的整数IP进行设置
- 由于bind函数提供的是通用参数类型,因此在传入结构体地址时还需要将
struct sockaddr_in*
强转为struct sockaddr*
类型- 包含头文件<netinet/in.h>和<arpa/inet.h>
字符串IP和整数IP
IP地址表现形式
- 字符串IP:类似于
40.77.167.60
这种字符串形式的IP地址,叫做基于字符串的点分十进制IP地址。- 整数IP:IP地址在进行网络传输时所用的形式,用一个32位的整数来表示IP地址。
一个整形四个字节,每个字节八个比特位,用来表示一个IP地址
用位段结构体表示一个IP地址
inet_addr
in_addr_t inet_addr(const char *cp);
功能:将字符串IP转换成整数IP
参数:
- cp: 点分十进制的字符串IP
返回值: 整数IP
inet_ntoa
char *inet_ntoa(struct in_addr in);
功能:将整数IP转换成字符串IP
参数:
- in_addr:描述IP地址的结构体
返回值: 字符串IP,这个结果存放到静态区了,第二次调用的结果会覆盖第一次的值
bind
int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);
功能:绑定端口号
参数:
- sockfd:套接字,设置的电话
- addr:传入
sockaddr_in
的结构体,使用时,注意要强转,struct sockaddr_in * 和 struct sockaddr * 是不同的类型,新电话号码和地址信息- addrlen:传入的addr结构体的长度,有多少信息
返回值: 成功返回0,失败返回-1
服务器启动
服务器实际执行的是一个死循环代码,将读取到的数据当作字符串看待,将读取到的数据的最后一个位置设置为
'\0'
客户端的端口号此时是网络序列,我们需要调用ntohs函数将其转为主机序列,获取到的客户端的IP地址是整数IP,我们需要通过调用inet_ntoa函数将其转为字符串IP,注意读取失败不要将服务器退出void start(){ char inbuf[1024];//读取缓冲区 char outbuf[1024];//发送缓冲区 while(1){ struct sockaddr_in peer;// 获取远端数据和信息,输出型参数 socklen_t len = sizeof(peer);// 远端的数据长度,输入输出型参数 ssize_t size = recvfrom(_sockfd, inbuf, sizeof(inbuf)-1, 0, (struct sockaddr*)&peer, &len); if (size > 0){ inbuf[size] = '\0';//当作字符串 int port = ntohs(peer.sin_port);//拿到了对方的port,网络转本地 string ip = inet_ntoa(peer.sin_addr);//拿到了对方的ip,整数IP转字符串IP cout << ip << ":" << port << "# " << inbuf << endl; } else{ cout << "recvfrom error" << endl; } } }
recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr* src_addr, socklen_t *addrlen);
作用: 服务器读取数据,可以用于UDP,
函数参数:
- sockfd:从该套接字获取信息
- buf:缓冲区指针,存放接收数据
- len:缓冲区长度
- flags:表示阻塞读取,一般写0
- src_addr:一个输出型参数,使用时需要强转为struct sockaddr* 的指针,存放数据发送方地址
- addrlen:输入输出型参数,记录src_addr结构体实际填充大小
返回值: 读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。
注意:
- 由于recvfrom函数提供的参数也是
struct sockaddr*
类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*
类型进行强转
主函数
agrv数组里面存储的是字符串,而端口号是一个整数,因此需要使用atoi函数将字符串转换成整数。然后我们就可以用这个IP地址和端口号来构造服务器了,服务器构造完成并初始化后就可以调用Start函数启动服务器。
输入格式是这样,./server.cpp port [ip],port一般写8080或8081
//./server.cpp port [ip] int main(int argc, char* argv[]) { if (argc != 2 && argc != 3){ cout << "Usage: " << argv[0] << " port" << endl; return 3; } int port = atoi(argv[1]);//port string ip; if(argc == 3){ ip = argv[2]; } UdpServer svr = UdpServer(port, ip); svr.InitServer(); svr.start(); return 0; }
结果:套接字创建成功、绑定成功,现在服务器就在等待客户端向它发送数据
[Jungle@VM-20-8-centos:~/lesson37]$ ./server 8080 xx.xxx.xx.xxx socket create success, sockfd: 3 bind port success, port: 8080
netstat
netstat
命令来查看当前网络的状态
netstat
常用选项说明:
- -n:直接使用IP地址,而不通过域名服务器。
- -l:显示监控中的服务器的Socket。
- -t:显示TCP传输协议的连线状况。
- -u:显示UDP传输协议的连线状况。
- -p:显示正在使用Socket的程序识别码和程序名称。
Proto
表示协议的类型,Recv-Q
表示网络接收队列,Send-Q
表示网络发送队列,Local Address
表示本地地址,Foreign Address
(0.0.0.0:*
表示任意IP地址、任意的端口号的程序都可以访问当前进程)表示外部地址,State
表示当前的状态,PID
表示该进程的进程ID,Program name
表示该进程的程序名称。[Jungle@VM-20-8-centos:~/lesson37]$ netstat -nua Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State udp 0 0 0.0.0.0:68 0.0.0.0:* udp 0 0 10.0.20.8:123 0.0.0.0:* udp 0 0 127.0.0.1:123 0.0.0.0:* udp6 0 0 fe80::5054:ff:fe08::123 :::* udp6 0 0 ::1:123 :::*
🏆2.客户端
用一个类封装客户端,里面包含的成员需要有套接字,远端端口号和远端IP
下面是框架:
class UdpClient { public: UdpClient(int server_port, string server_ip) :_sockfd(-1) ,_server_port(server_port) ,_server_ip(server_ip) {} void UdpClientInit(){} void start(){} ~UdpClient() { if (_sockfd >= 0){ close(_sockfd); } } private: int _sockfd; //文件描述符 int _server_port; //服务端端口号 string _server_ip; //服务端IP地址 };
创建套接字
客户端创建套接字时选择的协议家族也是
AF_INET
,需要的服务类型也是SOCK_DGRAM
,不用绑定void UdpClientInit() { // 创建套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0){ cerr << "sockfd creat fail" << endl; exit(1); } cout << "sockfd creat success, sockfd: " << _sockfd << endl; // 不需要绑定端口号,sendto会自动分配一个,且该端口号会变 }
客户端绑定
由于是网络通信,服务端和客户端都需要有各自的IP地址和端口号,但是客户端不需要绑定。
因为服务器是提供服务的,需要要让别人知道自己的IP地址和端口号,而且不能随意变更,否则别人寻找困难
客户端如果绑定了某个端口号,如果该端口号被别别人占用了,那么这个客户端就无法启动了,所以客户端端口号是可以变化的
客户端启动
现在客户端要发送数据给服务端,我们可以让客户端获取用户输入,不断将用户输入的数据发送给服务端。
客户端中存储的服务端的端口号此时是主机序列,我们需要调用htons函数将其转为网络序列后再设置进
struct sockaddr_in
结构体,客户端中存储的服务端的IP地址是字符串IP,我们需要通过调用inet_addr函数将其转为整数IP后再设置进struct sockaddr_in
结构体void start() { //填写服务器对应的信息 struct sockaddr_in peer; memset(&peer, '\0', sizeof(peer)); peer.sin_family = AF_INET; peer.sin_port = htons(_server_port); peer.sin_addr.s_addr = inet_addr(_server_ip.c_str()); string buffer; while(1){ cout << "Please Enter# "; getline(cin, buffer); sendto(_sockfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr*)&peer, sizeof(peer)); } }
sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
作用: 从一个套接字中获取信息
函数参数:
- sockfd:套接字描述符
- buf:发送数据缓冲区指针
- len:发送多少数据
- flags:表示阻塞发送,一般是0
- dest_addr:指向目标地址结构体的指针,包含目标IP和端口号信息
- addrlen:目标地址结构体长度
返回值: 成功返回实际写入的数据大小,失败返回-1
注意:
- 由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
- 由于sendto函数提供的参数也是
struct sockaddr*
类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*
类型进行强转。
🏆3.代码测试
本地测试
运行客户端时直接在后面跟上对应服务端的IP地址和端口号即可,本地测试时,IP地址为
127.0.0.1
代表本主机//./udpClient _server_port _server_ip int main(int argc, char* argv[]) { if (argc != 3){ cout << "Usage: " << argv[0] << " port" << endl; return 2; } int port = atoi(argv[1]);//port string ip = argv[2]; //ip, 127.0.0.1; UdpClient svr = UdpClient(port, ip); svr.UdpClientInit(); svr.start(); return 0; }
结果:客户端运行之后提示我们进行输入,当我们在客户端输入数据后,客户端将数据发送给服务端,此时服务端再将收到的数据打印输出
[Jungle@VM-20-8-centos:~/lesson37]$ ./client 8080 127.0.0.1 sockfd creat success, sockfd: 3 Please Enter# 3 Please Enter# 123 ----------------------------------------------- [Jungle@VM-20-8-centos:~/lesson37]$ ./server 8080 socket create success, sockfd: 3 bind port success, port: 8080 127.0.0.1:60308# 3 127.0.0.1:60308# 123
网络测试
静态编译客户端
client:client.cpp g++ -o $@ $^ -std=c++11 -static
然后用下面命令将,客户端从linux下载到本地,然后再从本地下载到客户端
rz // 上传文件 sz 文件// 发出文件
下载完文件后用chmod-x client修改文件权限之后,然后启动程序,连接对应的端口号和IP
windows和Linux通信
我们在vs2019中编写一下代码,设置我们想发送的ip地址,然后生成可执行程序,之后我们打开,Linux服务器,两者都要设置程一样端口号,就可以实现windows和Linux通信了。
我们发现windows下,除了开头启动信息和结尾清除启动信息外,其他都和Linux一样
#pragma warning(disable:4996)//消除报警 #define _CRT_SECURE_NO_WARNINGS 1 #pragma comment(lib, "Ws2_32.lib") // 需要包含的链接库 #include <stdio.h> #include <iostream> #include <string> #include <stdlib.h> #include <WinSock2.h> // windows socket 2.2版本 using namespace std; int _server_port = 8080;//端口号 string _server_ip = "xx.xxx.xx.xx";//IP int main(void) { WSADATA wsaData; // 用作初始化套接字 // 初始化启动信息(初始套接字) WSAStartup(MAKEWORD(2, 2), &wsaData); // 创建套接字 SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { cerr << "sockfd creat fail" << endl; exit(1); } cout << "sockfd creat success, sockfd: " << sockfd << endl; // 不需要绑定端口号,sendto会自动分配一个,且该端口号会变 //填写服务器对应的信息 struct sockaddr_in peer; memset(&peer, '\0', sizeof(peer)); peer.sin_family = AF_INET; peer.sin_port = htons(_server_port); peer.sin_addr.s_addr = inet_addr(_server_ip.c_str()); string buffer; while (1) { cout << "Please Enter# "; getline(cin, buffer); sendto(sockfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr*)&peer, sizeof(peer)); } closesocket(sockfd); // 释放套接字 WSACleanup(); // 清空启动信息 return 0; }
回响服务器
服务器端:当服务端收到客户端发来的数据后,除了在服务端进行打印以外,服务端可以调用sento函数将收到的数据重新发送给对应的客户端,服务端在调用sendto函数时需要传入客户端的网络属性信息,但服务端现在是知道客户端的网络属性信息的,因为服务端在此之前就已经通过recvfrom函数获取到了客户端的网络属性信息
void start(){ char inbuf[1024];//读取缓冲区 char outbuf[1024];//发送缓冲区 while(1){ struct sockaddr_in peer;// 获取远端数据和信息,输出型参数 socklen_t len = sizeof(peer);// 远端的数据长度,输入输出型参数 ssize_t size = recvfrom(_sockfd, inbuf, sizeof(inbuf)-1, 0, (struct sockaddr*)&peer, &len); if (size > 0){ inbuf[size] = '\0';//当作字符串 int port = ntohs(peer.sin_port);//拿到了对方的port,网络转本地 string ip = inet_ntoa(peer.sin_addr);//拿到了对方的ip,整数IP转字符串IP cout << ip << ":" << port << "# " << inbuf << endl; } else{ cout << "recvfrom error" << endl; } //服务器向客户端发送 string echo_msg = "server get!->"; echo_msg += inbuf; sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer)); } }
客户端:由于服务端还会将该数据重新发给客户端,因此客户端发完数据后还需要调recvfrom来读取服务端发来的响应数据,在客户端调用recvfrom函数接收服务端发来的响应数据时,客户端同时也需要读取服务端与网络相关的各种信息,客户端接收到服务端的响应数据后,将数据原封不动的打印出来就行了
void start() { //填写服务器对应的信息 struct sockaddr_in peer; memset(&peer, '\0', sizeof(peer)); peer.sin_family = AF_INET; peer.sin_port = htons(_server_port); peer.sin_addr.s_addr = inet_addr(_server_ip.c_str()); string buffer; while(1){ cout << "Please Enter# "; getline(cin, buffer); sendto(_sockfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr*)&peer, sizeof(peer)); //客户端接收服务器端信息 char buffer[1024]; struct sockaddr_in tmp; socklen_t len = sizeof(tmp); ssize_t size = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&tmp, &len); if (size > 0){ buffer[size] = '\0'; cout << buffer << endl; } } }
结果:在服务端和客户端就都能够看到对应的现象
[Jungle@VM-20-8-centos:~/lesson37]$ ./client 8080 127.0.0.1 sockfd creat success, sockfd: 3 Please Enter# 123 server get!->123 ------------------------------------------------------ [Jungle@VM-20-8-centos:~/lesson37]$ ./server 8080 socket create success, sockfd: 3 bind port success, port: 8080 127.0.0.1:53473# 123
多人聊天室
多人聊天室本质是,服务器将受到的消息发送给每一个成员,所以首先我们要用checkOnlineUser统计在线人数,用messageRoute将消息发送给所有人
服务器端:创建一个成员变量users统计在线用户,key存放的是ip和端口号,value存放的是用户说的话
通信时,checkOnlineUser统计在线人数,在map中找不到,则添加,如果找到了则忽略,checkOnlineUser将收到的每一条消息都遍历发送给所有人
class UdpServer { public: UdpServer(int port, string ip) : _sockfd(-1), _port(port), _ip(ip){}; void InitServer() { // 创建套接字 _sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (_sockfd < 0) { // 创建套接字失败 cout << "socket error" << endl; exit(1); } cout << "socket create success, sockfd: " << _sockfd << endl; // 填充网络通信相关信息 struct sockaddr_in local; memset(&local, '\0', sizeof(local)); // 使用前先初始化结构体 local.sin_family = AF_INET; // 填充协议家族,选择本地还是网络 local.sin_port = htons(_port); // 将主机序列的端口号转为网络序列的端口号,由于会通过网络发送,所以要从本地序列转化为网络序列 // local.sin_addr.s_addr = inet_addr(_ip.c_str());// 直向特定的IP绑定 local.sin_addr.s_addr = INADDR_ANY; // 取消单个ip绑定,可以接受来自任意client的请求,从任意ip获取数据,强烈推荐 // 绑定 if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) == -1) { cout << "bind fail" << endl; exit(2); } cout << "bind port success, port: " << _port << endl; } void start() { char inbuf[1024]; // 读取缓冲区 char outbuf[1024]; // 发送缓冲区 while (1) { struct sockaddr_in peer; // 获取远端数据和信息,输出型参数 socklen_t len = sizeof(peer); // 远端的数据长度,输入输出型参数 ssize_t size = recvfrom(_sockfd, inbuf, sizeof(inbuf) - 1, 0, (struct sockaddr *)&peer, &len); if (size > 0) { inbuf[size] = '\0'; // 当作字符串 int port = ntohs(peer.sin_port); // 拿到了对方的port,网络转本地 string ip = inet_ntoa(peer.sin_addr); // 拿到了对方的ip,整数IP转字符串IP cout << ip << ":" << port << "# " << inbuf << endl; } else { cout << "recvfrom error" << endl; } // 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port] string peerIp = inet_ntoa(peer.sin_addr); // 拿到了对方的IP,将整数转为字符串 int peerPort = ntohs(peer.sin_port); // 拿到了对方的port checkOnlineUser(peerIp, peerPort, peer);// 检查在线用户 cout << "[IP]:" << peerIp.c_str() << "[prot]:" << peerPort << "# " << inbuf << endl; messageRoute(peerIp, peerPort,inbuf); //消息路由 // //服务器向客户端发送 // string echo_msg = "server get!->"; // echo_msg += inbuf; // sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)&peer, sizeof(peer)); } } // 检查在线用户,如果不存在就添加,如果存在就什么也不做 void checkOnlineUser(string &ip, int port, struct sockaddr_in &peer) { string key = ip; key += ":"; key += to_string(port); auto it = users.find(key); if (it == users.end()) { // 不存在就插入 users.insert({key, peer}); } } //将收到的消息转发给所有人 void messageRoute(string ip, int port, string info) { string message = "["; message += ip; message += ":"; message += 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)); } } ~UdpServer() { if (_sockfd >= 0) { close(_sockfd); } }; private: int _sockfd; // 文件描述符 int _port; // 端口号 string _ip; // IP地址 unordered_map<string, struct sockaddr_in> users; // 在线用户 };
客户端:建立两个线程,主线程发送数据,次线程接收数据
#include <iostream> #include <string> #include <cstring> #include <stdlib.h> #include <cstdlib> #include <cassert> #include <unistd.h> #include <strings.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <pthread.h> using namespace std; // 填写服务器对应的信息 struct sockaddr_in server; void *recverAndPrint(void *args) { while (1) { int sockfd = *(int *)args;//将套接字内容强转 char buffer[1024]; struct sockaddr_in temp; socklen_t len = sizeof(temp); ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len); if (s > 0)//成功打印结果 { buffer[s] = '\0'; cout << buffer << endl; } } } //./udpClient _server_port _server_ip int main(int argc, char *argv[]) { if (argc != 3) { cout << "Usage: " << argv[0] << " port" << endl; return 2; } int port = atoi(argv[1]); // port string ip = argv[2]; // ip, 127.0.0.1; // 创建套接字 int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { cerr << "sockfd creat fail" << endl; exit(1); } cout << "sockfd creat success, sockfd: " << sockfd << endl; // 不需要绑定端口号,sendto会自动分配一个,且该端口号会变 memset(&server, '\0', sizeof(server)); server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = inet_addr(ip.c_str()); pthread_t t; pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd); string buffer; while (1) { getline(cin, buffer); sendto(sockfd, buffer.c_str(), buffer.size(), 0, (struct sockaddr *)&server, sizeof(server)); } return 0; }
结果:用mkfifo fifo创建管道后,启动服务器,客户端向管道中写入,同时我们在管道另一端将内容打印出来
//服务器接收到的内容 [Jungle@VM-20-8-centos:~/lesson37]$ ./server 8080 socket create success, sockfd: 3 bind port success, port: 8080 127.0.0.1:52790# 123 [IP]:127.0.0.1[prot]:52790# 123 ----------------------------------------- //客户端向管道中写入的数据 [Jungle@VM-20-8-centos:~/lesson37]$ ./client 8080 127.0.0.1 > fifo 123 ----------------------------------------- //将管道中内容打印出来 [Jungle@VM-20-8-centos:~/lesson37]$ cat < fifo sockfd creat success, sockfd: 3 [127.0.0.1:52790]# 123