目录

背景知识

主机间通信本质

socket

端口号特点:

为什么不用进程pid?

网络字节序

socket编程接口API

sockaddr结构

​编辑 简单UDP网络程序

了解UDP协议

简易多人聊天室实现

服务端代码:

客户端代码:


背景知识

主机间通信本质

各自主机上的进程之间相互交互数据

IP地址完成主机与主机之间的通信

主机上各自的通信进程分别是发送数据和接收数据的一方

socket

IP地址:标识主机唯一性(4字节32位)

端口号port:标识了主机上的进程唯一性(2字节16位)

那么 IP地址 + 端口号 就能够标识网络上的某一台主机的某一个进程,将IP地址+端口号称为socket对,之间用冒号分隔,如 源IP:源端口号  目的IP:目的端口号。

端口号特点:

端口号 (port) 是传输层协议的内容 .
端口号是一个2字节16位的整数;
端口号用来标识一个进程

一个进程可以绑定多个端口号

一个端口号只能被一个进程占用

OS内部用哈希表存储端口号,通过哈希表映射,使用端口号可以快速找到进程

为什么不用进程pid?

避免进程管理和网络通信的强耦合 ,同时端口号标识的进程是要进行网络通信的网络进程,没有端口号则说明是本地进程,不进行网络通信,就好比身份证号与学号,独立分配,便于管理。

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分 , 磁盘文件中的多字节数据相对于文件中的 偏移地址也有大端小端之分, 网络数据流同样有大端小端之分。

在网络中发送主机将发送缓冲区的数据按内存从低到高地址顺序发出,接收主机保存数据在接收缓冲区从低到高。

TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。

所有主机都会按照这个TCP/IP规定的网络字节序来发送/接收数据,如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库中的四个函数做网络字节序和主机字节序的转换,当主机是小端字节序,函数内部才会将参数做转换,否则原封不动返回。

h表示host,n表示network,l表示32位长整数,s表示16位短整数。

socket编程接口API

TCP或UDP,客户端/服务器

创建socket文件描述符:

int socket(int domain, int type, int protocol);

TCP或UDP,服务器

绑定端口号:

int bind(int socket, const struct sockaddr *address,socklen_t address_len);

TCP,服务器

监听socket:

int listen(int socket, int backlog);

接收请求:

int accept(int socket, struct sockaddr* address,socklen_t* address_len);

TCP,客户端

建立连接:

int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6(网络通信)以及UNIX Domain Socket(域间通信)。在网络中有描述网络地址的结构体sockaddr。

struct sockaddr_in结构体是描述网络通信

sockaddr_un结构体是描述域间通信

两个类型都可以用struct sockaddr 类型表示,在使用接口时进行强制类型转换就行了,因为他们前面16位字段表示地址类型,底层根据16位地址类型进行类型转换就可以适用不同类型的地址了。

在内核代码中可以看到sockaddr_in描述网络通信地址的结构体中有字段表示IP地址和端口号port:

 简单UDP网络程序

了解UDP协议

UDP协议是传输层协议

特点:

无连接:不用提前建立连接,类似邮箱,谁都能往邮箱发消息

不可靠传输:与TCP相比没有超时重传等机制保证丢包重传,数据传输是不可靠的

面向数据报:意味着应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并,比如用UDP传输100个字节的数据如果发送端调用一次sendto, 发送100个字节, 那么接收端也必须调用对应的一次recvfrom, 接收100个字节; 而不能循环调用10次recvfrom,每次接收10个字节

支持全双工:读写使用同一个套接字,UDP调用sendto会将数据直接交给内核,内核直接将数据交给网络层,UDP有接收缓冲区,但是不能保证发送报文的顺序和接收的一致,缓冲区满了之后再到达的报文会被丢弃。

简易多人聊天室实现

服务端代码:

1.创建套接字int socket(int domain,int type,int protocol):

参数说明:

domain:域,本地(AF_UNIX)或网络(AF_INET)

type:报文类型,流式(SOCK_STREAM),用户数据报(SOCK_DGRAM)

protocol:协议类型,在网络应用中填充0

返回值:返回一个文件描述符

2.绑定网络信息,先填充协议家族,指明ip和port

先填充基本信息到sockaddr_in网络地址结构体

2.1填充协议家族,域:.sin_family

2.2填充端口号:.sin_port,需用htons转化

2.3填充IP地址sin_addr.s_addr

使用inet_addr函数指定填充确定的IP,内部自动调用hton

IP地址填充INADDR_ANY表示绑定服务器上的所有IP,因为云服务器禁止绑定确定的IP,为了安全性云服务器上的IP是模拟出来的。

IP互相转化函数

inet_ntoa:将四字节ip转化为点分十进制字符串

返回的地址存储在静态存储区,下一次调用的时候会覆盖上一次的结果,这个函数可能是非线程安全函数,取决不同平台的实现

 inet_addr:将点分十进制字符串转化成四字节IP,内部自动hton,这个函数未必是线程安全函数

其他IP地址转化函数也类似:

在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题:

2.4bind网络信息

参数说明:
sockfd:绑定的文件描述符

addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。

addrlen:传入的addr结构体的长度。

3.收发消息

往套接字发送消息:

使用sendto函数往套接字发送消息:

表示往sockfd套接字发送buf中的len长度的内容,flag填0表示像write一样阻塞读取,后两个参数是输入型参数,表示要发送的对端主机信息(IP,端口号)。

从套接字收消息:

这个函数与上面类似,将从sockfd套接字中读取len长度字节数据到buf中,后两个参数主要是做输出型参数,可以从中提取消息来源的主机信息(IP,端口号)。

注意:recvfrom和sendto是专门用于udp收发用户数据报的

实现多人聊天室的功能:让服务器作为数据的中间收发者,客户端发送数据给服务端,服务端再将数据广播给所有(除了发送方)客户端,为了维护多个客户端信息,可以在服务端收取数据的时候提取发送方的主机信息,将其存储在unordered_map中方便广播。

实现日志功能:引入日志,使用可变参数函数实现日志函数,方便格式化输出,顺便包含各式头文件方便服务器代码和客户端代码引用。

在log.hpp中:

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdarg.h>
#include <time.h>
#include <string>
#include <unordered_map>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <string>
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3using namespace std;
const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};
void logMessage(int level, const char *format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);char *name = getenv("USER");char logInfo[1024];va_list ap;va_start(ap, format);                            // 第一个知道类型的参数formatvsnprintf(logInfo, sizeof(logInfo), format, ap); // 以format格式写入logInfo,自动传递参数va_end(ap);                                      // ap=NULLFILE *out = level == FATAL ? stderr : stdout;fprintf(out, "%s | %u | %s | %s\n",log_level[level],(unsigned int)time(nullptr),name == nullptr ? "unknown" : name,logInfo);
}

服务器代码实现:

#include "log.hpp"class udpServer
{
private:uint16_t port_;string ip_;int sockfd_;unordered_map<string, struct sockaddr_in> users_; // ip:port peer
public:udpServer(int port, string ip = ""): port_((uint16_t)port), ip_(ip), sockfd_(-1){}~udpServer() {}void init(){// 1.创建socket,打开文件sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd_ < 0){logMessage(FATAL, "socket : %s:%d", strerror(errno), sockfd_);exit(1);}// 2.绑定网络信息,指明ip和portstruct sockaddr_in local;bzero(&local, sizeof(local));// 2.1填充协议家族,域local.sin_family = AF_INET;// 2.2填充服务器对应的端口号local.sin_port = htons(port_);// 2.3填充IP地址     //将点分十进制转化成四字节iplocal.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());// 2.4绑定网络信息if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) == -1){logMessage(FATAL, "bind : %s:%d", strerror(errno), sockfd_);}cout << "create server success" << endl;}void start(){char inbuffer[1024];char outbuffer[1024];while (1){// 往peer写入客户端信息struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1,0, (struct sockaddr *)&peer, &len);if (s > 0){inbuffer[s] = '\0';}else{logMessage(WARINING, "recvfrom : %s:%d", strerror(errno), sockfd_);continue;}// 取出客户端的ip和portstring peerIp = inet_ntoa(peer.sin_addr);uint32_t peerPort = ntohs(peer.sin_port);logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);broadcastMessage(peerIp, peerPort, peer, inbuffer); // 如果是新用户 就先添加用户}}void broadcastMessage(string peerIp, uint32_t peerPort, struct sockaddr_in peer, char *send){string socket = peerIp + ":";socket += to_string(peerPort);auto it = users_.find(peerIp);if (it == users_.end())users_.insert({socket, peer});string message="FROM";message += "[";message += peerIp;message += ":";message += to_string(peerPort);message += "]";message += " echo# ";message += send;for (auto &user : users_){if (user.first != socket){sendto(sockfd_, message.c_str(), message.size(),0, (struct sockaddr *)&user.second, sizeof(user.second));}}}
};int main(int argc, char *argv[])
{using namespace std;if (argc != 2 && argc != 3){cout << "Usage:\n\t " << argv[0] << " port [ip]" << endl;exit(3);}uint16_t port = atoi(argv[1]);string ip;if (argc == 3)ip = argv[2];udpServer svr(port, ip);svr.init();svr.start();return 0;
}

客户端代码:

客户端工作较为简单,直接创建套接字,然后就可以往服务器收发消息了,

客户端不需要bind,指的是不需要用户自己bind端口信息,os会自动bind,而且客户端不能绑定指定端口,因为端口可能被别的客户端使用,导致客户端无法启动,而服务端提供的服务需要被所有人知道,所以不能随便改变端口号,需要显式bind指定端口。

当客户端首次调用sendto函数时,函数内部会自动绑定(bind)。

为了避免多个客户端收发的线程一样导致输入输出的卡顿,创建一个新线程用于收取服务器发来的消息,原来的主线程用于发消息给服务器。

#include "log.hpp"void *recvAndPrint(void *args)
{int sockfd = *(int *)args;char buffer[1024];struct sockaddr_in temp;socklen_t len = sizeof(temp);while (1){ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer) - 1,0, (struct sockaddr *)&temp, &len);if (s > 0){buffer[s] = 0;cout << buffer<< endl;}}return nullptr;
}
struct sockaddr_in server;int main()
{string ip = "127.0.0.1";//表示本地环回,给本主机发消息uint16_t port = 8080;int sockfd = socket(AF_INET, SOCK_DGRAM, 0);assert(sockfd > 0);bzero(&server, 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, recvAndPrint, (void *)&sockfd);// 往服务器发送信息string buffer;while (1){cout << "Please Enter# ";std::getline(std::cin, buffer);sendto(sockfd, buffer.c_str(), buffer.size(),//首次调用时自动bind0, (struct sockaddr *)&server, sizeof(server));}close(sockfd);return 0;
}

效果演示(3人通信):因为客户端发消息才会bind,所以后发消息的客户端看不到最先发消息的客户端的前面的消息。

网络编程套接字(上篇)UDP实现简易多人聊天室相关推荐

  1. socket网络编程套接字TCP/UDP两种方式详解

    目录 准备知识 源IP地址和目的IP地址 端口号与进程ID 传输层协议--TCP 传输层协议--UCP 网络字节序 socket套接字介绍 概念 常见的三种socket socket编程常见API s ...

  2. 基于UDP的简易多人聊天室

    1.服务器 1.服务器采用了线程进行编写,这样可以共享链表,比较简单. 2.实现思路:由客户端发送消息(内容包含:客户端自定义的名字.客户端发送消息的类别.客户端发送的消息内容),服务器接收到消息后根 ...

  3. 详细讲解 —— 网络编程套接字(Java EE初阶)

    网络编程套接字 1 认识网络编程套接字 2 UDP 数据报套接字编程 2.1 UPD服务端 2.1 UDP客户端 2.3 结果测试 3 TCP流套接字编程 3.1 TCP服务端 3.2 TCP客户端 ...

  4. 网络编程套接字(二)

    网络编程套接字(二) 文章目录 网络编程套接字(二) 一.简单的UDP网络程序 一.简单的UDP网络程序 封装udp_socket #pragma once #include <cstdio&g ...

  5. 网络编程套接字(一)

    网络编程套接字(一) 文章目录 网络编程套接字(一) 一.基础知识 二.sock编程接口 一.基础知识 1. 理解源IP地址和目的IP地址 在IP数据包头部中,有两个IP地址,分别叫源IP地址和目的I ...

  6. 【javaEE】网络编程套接字

    To u&me: 努力经营当下,直至未来明朗 文章目录 前言 一.网络编程(没时间可以跳过) 一)网络编程了解 二)相关基本概念 二.Socket套接字 三.数据报套接字通信(UDP) 写一个 ...

  7. Java网络编程套接字

    文章目录 1.网络编程基础 2.什么是网络编程 3.网络编程中的基本概念 3.1.发送端和接收端 3.2 请求和响应 3.3 客户端和服务端 3.4 常见的客户端服务端模型 4.Socket套接字 4 ...

  8. UNIX网络编程---套接字编程简介(三)

    UNIX网络编程---套接字编程简介 一.概述 从这里开始正式开始网络编程之旅,所有的函数都是基本的库函数.这些都是网络编程的基础.Come on!!!! 二.套接字地址结构 大多数套接字函数都需要一 ...

  9. 【网络编程套接字(一)】

    网络编程套接字(一) 理解源IP地址和目的IP地址 理解源MAC地址和目的MAC地址 理解源端口号和目的端口号 PORT VS PID 认识TCP协议和UDP协议 网络字节序 socket编程接口 s ...

最新文章

  1. 模拟器抓取https方法
  2. 精通python设计模式-Python设计模式
  3. C++ Heavy Light Decomposition重轻分解的实现算法(附完整源码)
  4. android studio run按钮为灰色
  5. 23V3有这种C语言表达式吗,数据结构(C语言版第2版_李云清)习题答案2012-12.doc
  6. 【软件开发底层知识修炼】二十八 C/C++中volatile的作用
  7. java垃圾回收机制_干货:Java 垃圾回收机制
  8. 互联网晚报 | 3月15日 星期二 |​ 特斯拉Model 3高性能版和Model Y长续版再涨价;字节成都成立光合科技公司...
  9. 记录几个CentOS安装包(rpm)的下载地址-离线安装必备
  10. @datetimeformat注解使用 晚了8个小时_Java注解,就是那么简单
  11. C++进阶教程之命名空间
  12. windows10 安装 rancher desktop及测试案例
  13. Biological Psychiatry:亚属连接预测经颅磁刺激位点抗抑郁疗效
  14. android email分析,QQ邮箱Android客户端产品体验报告
  15. 关于美国安利的真实情况 !!!!!
  16. 数据的基本类型有哪些
  17. 处理口罩图片数据(yolov5)中碰到的问题(macOS)
  18. 使用 cocos creator 3.0 制作抽奖小游戏
  19. 基于SpringBoot的企业OA系统的设计与实现,Java毕业设计项目,高质量毕业论文范例,源码,数据库脚本,项目导入运行视频教程,论文撰写教程
  20. Python旅途休憩——闭包

热门文章

  1. SOLIDWORKS标注尺寸时,小数点后的数值不显示怎么办?
  2. springboot bean生命周期
  3. 四旋翼飞行器17——APM飞控主要飞行模式
  4. 用Animation动画实现Android应用的欢迎界面
  5. linux关闭8080端口命令,linux iptables开放/关闭端口命令
  6. 安装Ubuntu+gpu+tensorflow+py2+py3
  7. setenforce: SELinux is disabled
  8. python读取fiddler_大数据采集之python的docker爬虫技术-fiddler抓包软件详细配置(7)...
  9. mysql更新语句用法_MySQL update 语句的正确用法
  10. 【java】反射机制