【网络编程】第二章 网络套接字(socket+UDP协议程序)

news/2024/3/29 23:42:58/文章来源:https://blog.csdn.net/YQ20210216/article/details/128102353

🏆个人主页:企鹅不叫的博客

​ 🌈专栏

  • C语言初阶和进阶
  • C项目
  • Leetcode刷题
  • 初阶数据结构与算法
  • C++初阶和进阶
  • 《深入理解计算机操作系统》
  • 《高质量C/C++编程》
  • Linux

⭐️ 博主码云gitee链接:代码仓库地址

⚡若有帮助可以【关注+点赞+收藏】,大家一起进步!

💙系列文章💙

【网络编程】第一章 网络基础(协议+OSI+TCPIP+网络传输的流程+IP地址+MAC地址)


文章目录

  • 💙系列文章💙
  • 💎一、概念介绍
    • 🏆1.源IP地址和目的IP地址
    • 🏆2.源MAC地址和目的MAC地址
    • 🏆3.端口号
    • 🏆4.端口号PORT VS 进程PID
    • 🏆5.TCP协议和UDP协议
    • 🏆6.网络字节序
  • 💎二、socket常见API
    • 🏆1.socket的API
    • 🏆2.sockaddr结构
  • 💎三、UDP协议程序
    • 🏆1.服务端
      • 创建套接字
      • socket
      • 服务器绑定
      • 字符串IP和整数IP
      • inet_addr
      • inet_ntoa
      • bind
      • 服务器启动
      • recvfro
      • 主函数
      • netstat
    • 🏆2.客户端
      • 创建套接字
      • 客户端绑定
      • 客户端启动
      • sendt
    • 🏆3.代码测试
      • 本地测试
      • 网络测试
      • windows和Linux通信
      • 回响服务器
      • 多人聊天室


💎一、概念介绍

🏆1.源IP地址和目的IP地址

IP地址是用来标识网络中不同主机的地址。

  • 源IP地址: 发送方主机的IP地址,保证响应主机“往哪放”
  • 目的IP地址: 接收方主机的IP地址,保证发送方主机“往哪发”

🏆2.源MAC地址和目的MAC地址

MAC地址是网卡决定的,是固定的。

  • 目标MAC地址就是对方的MAC地址;
  • 源MAC地址就是你自己的MAC地址。

🏆3.端口号

端口号(port)的作用实际就是标识一台主机上的一个进程。

  • 端口号是传输层协议的内容。
  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理。
  • 一个端口号只能被一个进程占用。

IP地址能够唯一标识公网内的一台主机,而端口号能够唯一标识一台主机上的一个进程,因此用IP地址+端口号就能够唯一标识网络上的某一台主机的某一个进程

  • 源端口号: 发送方主机的服务进程绑定的端口号,保证接收方能够找到对应的服务
  • 目的端口号: 接收方主机的服务进程绑定的端口号,保证发送方能够找到对应的服务

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位长整形
  • 如果主机是小端字节序,函数会对参数进行处理,进行大小端转换
  • 如果主机是大端字节序,函数不会对这些参数处理,直接返回

💎二、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);

🏆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结构体指针为参数

💎三、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:协议家族,我们用的都是IPV4,这里会填AF_INET,相当于struct sockaddr结构的前16个位
  • 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的结构体,里面记录这本地的信息:sin_family(协议家族)、sin_port(端口号,16位的整数)和sin_addr(IP地址,一个32位的整数),用来进行绑定,使用时,注意要强转
  • 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转字符串IPcout << ip << ":" << port << "# " << inbuf << endl;}else{cout << "recvfrom error" << endl;}}}

recvfro

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr* src_addr, socklen_t *addrlen); 

作用: 服务器读取数据,从一个套接字中获取信息,面向无连接,

函数参数:

  • sockfd:从该套接字获取信息
  • buf:缓冲区,把数据读取到该缓冲区中
  • len:一次读多少自己的数据
  • flags:表示阻塞读取,一般写0
  • src_addr:一个输出型参数,获取到对端的信息,有端口号,IP等,方便后序我们对其进行响应
  • 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]);//portstring ip;if(argc == 3){ip = argv[2]; //ip,43.138.73.215}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));}
}

sendt

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:本地的网络相关属性信息,要填充好发送给对方,确保对方能够响应
  • addrlen:dest_addr的实际大小

返回值: 成功返回实际写入的数据大小,失败返回-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]);//portstring 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.cppg++ -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";//IPint 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转字符串IPcout << 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转字符串IPcout << 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); // 拿到了对方的portcheckOnlineUser(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]); // portstring 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
-----------------------------------------
//z
[Jungle@VM-20-8-centos:~/lesson37]$ cat < fifo
sockfd creat success, sockfd: 3
[127.0.0.1:52790]# 123

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_38663.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java-泛型实验

1.定义一个学生类Student&#xff0c;具有年龄age和姓名name两个属性&#xff0c;并通过实现Comparable接口提供比较规则&#xff08;返回两个学生的年龄差&#xff09;&#xff0c; 定义测试类Test&#xff0c;在测试类中定义测试方法Comparable getMax(Comparable c1, Compar…

Docker-JenKins安装及配置!

Jenkins官网&#xff1a;Jenkins 安装主机配置&#xff08;官方&#xff09;&#xff1a; 最低&#xff1a; 256 MB 内存 1 GB 的驱动器空间&#xff08;尽管如果将 Jenkins 作为 Docker 容器运行&#xff0c;则建议至少 10 GB&#xff09; 小团队推荐&#xff1a; 4 GB …

英文ppt怎么翻译成中文?教你几种ppt翻译方法

ppt文件受到很多人的喜欢是因为它展示起来直观清晰&#xff0c;无论是老师在课堂上使用ppt课件来教学&#xff0c;还是在工作汇报中用ppt文件展示设计的方案或取得的成果。但当我们需要把ppt文档里的文本内容里的外语翻译成中文的时候&#xff0c;我们应该怎么做呢&#xff1f;…

003. 电话号码的字母组合——回溯算法

1.题目链接&#xff1a; 17. 电话号码的字母组合 2.解题思路&#xff1a; 2.1.题目要求&#xff1a; 给定一个仅包含数字 2-9 的字符串 digits &#xff0c;返回所有它能表示的字母组合。 数字和字母的关系&#xff1a; 例子&#xff1a; 输入&#xff1a;"23" …

[Spring]第二篇:IOC控制反转

简单的说就是,创建对象的权利,或者是控制的位置,由JAVA代码转移到spring容器,由spring的容器控制对象的创建,就是控制反转. spring创建对象时,会读取配置文件,配置文件中主要配置接口和实现类的关系,每个接口对相应一个实现类,使用<bean>标签配置,<bean中的id可以随便…

学生选课系统

项目描述 通过项目背景的分析以及了解到现在学校面临的问题&#xff0c;特别需要一个选课管理系统保证学生信息以及各种课程成绩的准确性和实效性&#xff0c;通过利用计算机的高速计算和快速的统计分析&#xff0c;保证学生信息的最新记录。从教职工的角度老考虑&#xff0c;…

用VS软件开发“中国象棋“游戏<笔记摘录>

整体架构如上 1.很直观地去看这个中国象棋的界面,数一下它有多少行和多少列. 10行,9列:要注意这里数的是安放象棋的位置,有10行9列 这里我们首先想到的必然是二维数组,每一个行列交叉的点都设置成二维数组a[i][j]这样的格式,以此来确定棋盘上面每一个棋子的位置和走向. 我们…

详解 Spring Boot 项目中的配置文件

目录 1. Spring Boot 项目中配日文件的作用是什么 2. Spring Boot 配置文件的两种格式 3. properties 配置文件 3.1 properties 配置文件的基本语法 3.2 properties 配置文件的分类 3.3 如何读取配置文件 3.4 properties 配置文件的优缺点分析 4. yml 配置文件 4.1 yml …

BP神经网络PID从Simulink仿真到PLC控制实现(含博途PLC完整SCL源代码)

单神经元自适应PID控制博途PLC完整源代码,请参看下面的文章链接: 博途PLC单神经元自适应PID控制_RXXW_Dor的博客-CSDN博客_单神经元pid控制1、单神经元作为构成神经网络的基本单位,具有自学习和自适应能力,且结构简单易于计算,传统的PID具有结构简单、调整方便和参数整定…

【软件测试】8年资深测试说出来我们的心声......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 执着于手动的功能测…

SSM毕设项目 - 基于SSM的毕业设计管理系统(含源码+论文)

文章目录1 项目简介2 实现效果2.1 界面展示3 设计方案3.1 概述3.2 系统流程3.2.1 系统开发流程3.3.2 教师登录流程3.3.3 系统操作流程3.3 系统结构设计4 项目获取1 项目简介 Hi&#xff0c;各位同学好呀&#xff0c;这里是M学姐&#xff01; 今天向大家分享一个今年(2022)最新…

【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)

需要全部代码请点赞关注收藏后评论区留言私信~~~ 与传统的影视行业相比&#xff0c;诞生于移动互联网时代的短视频是个全新行业&#xff0c;它制作方便又容易传播&#xff0c;一出现就成为大街小巷的时髦潮流。 各行各业的人们均可通过短视频展示自己&#xff0c;短小精悍的视频…

社区系统项目复盘-6

文章目录什么是Elasticsearch&#xff1f;Spring是怎么整合Elasticsearch的&#xff1f;开发社区搜索功能Elasticsearch实现全文搜索功能什么是Elasticsearch&#xff1f; Elasticsearch简介 一个分布式的、Restful风格的搜索引擎支持对各种类型的数据的检索搜索速度快&#xf…

基于粒子群算法和遗传算法优化的高速列车横向悬挂

目录 前言 1.高速列车模型 2.优化算法优化模糊PID流程 3.普通PID、优化算法模糊PID仿真对比 3.1 模糊控制器设计 3.2 仿真结果 3.2.1粒子群优化PID 3.2.2粒子群优化模糊PID 3.2.3遗传算法优化模糊PID 4.总结 前言 高速列车&#xff0c;是指最高行驶速度在200km/h 及以…

小知识· Zigbee 简介

1. 介绍 ZigBee是一种近距离、低复杂度、低功耗、低速率、低成本的双向无线通讯技术 ZigBee建立在IEEE 802.15.4标准&#xff08;定义了PHY和MAC层&#xff09;之上&#xff0c;ZigBee联盟对其网络层和应用层进行了标准化 ZigBee协议栈可分为五层 - 物理层&#xff08;PHY&a…

多进程并发服务器

TCP三次握手建立连接错误处理模块&#xff1a;wrap.c,函数声明&#xff1a;wrap.h并发服务器模型&#xff08;多进程&#xff0c;多线程&#xff09; 转换大小写程序 服务端 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #incl…

Mybatis Plus 多租户id使用

本文就不多逼逼&#xff0c;直接进入正题。 什么是多租户 多租户技术&#xff08;Multi-TenancyTechnology&#xff09;又称多重租赁技术&#xff0c;简称SaaS&#xff0c;是一种软件架构技术&#xff0c;是实现如何在多用户环境下 &#xff08;此处的多用户一般是面向企业用…

Java SPI机制的使用和理解

前言&#xff1a; SPI(Service Provider Interface)&#xff0c;是JDK内置的一种服务提供发现机制&#xff0c;Java中 SPI 机制主要思想是将装配的控制权移到程序之外&#xff0c;在模块化设计中这个机制尤其重要&#xff0c;其核心思想就是解耦 1、大家都知道API&#xff0c;却…

【C++】STL —— map和set的模拟实现

目录 一、基础铺垫 二、基本结构分析 1. 节点结构分析 2. 模板参数中仿函数分析 三、正向迭代器 四、封装完成的红黑树 五、map的模拟实现 六、set的模拟实现 一、基础铺垫 在前面的博客中我们了解了map和set的基本使用&#xff0c;以及对二叉搜索树、AVL树和红黑树的…

IPv6进阶:IPv6 过渡技术之 NAT64(IPv6 节点主动访问 IPv4 节点-地址池方式)

实验拓扑 PC1是IPv4网络的一个节点&#xff0c;处于Trust安全域&#xff1b;PC2是IPv6网络的一个节点&#xff0c;处于Untrust安全域。 实验需求 完成防火墙IPv4、IPv6接口的配置&#xff0c;并将接口添加到相应的安全域&#xff1b;在防火墙上配置NAT64的IPv6前缀3001::/64&…