.net网站搭建,清溪网站建设,北京网站设计公司兴田德润怎么样,网站建设哪几家公司好目录 本文核心
预备知识
1.端口号
认识TCP协议
认识UDP协议
网络字节序
socket编程接口
sockaddr结构
UDP套接字编程
服务端
客户端
TCP与UDP传输的区别
可靠性#xff1a;
传输方式#xff1a;
用途#xff1a;
头部开销#xff1a;
速度#xff1a;
li…目录 本文核心
预备知识
1.端口号
认识TCP协议
认识UDP协议
网络字节序
socket编程接口
sockaddr结构
UDP套接字编程
服务端
客户端
TCP与UDP传输的区别
可靠性
传输方式
用途
头部开销
速度
linux相关的指令操作
ip地址转化函数 本文核心
认识IP地址, 端口号, 网络字节序等网络编程中的基本概念; 学习socket api的基本用法; 能够实现一个简单的udp客户端/服务器; 预备知识 1.端口号 端口号 (port) 是传输层协议的内容 . 端口号是一个 2字节16位的整数; 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理; IP地址 端口号能够标识网络上的某一台主机的某一个进程; 一个端口号只能被一个进程占用但是一个进程可以占用多个端口。 理解 端口号 和 进程 ID 我们之前在学习系统编程的时候 , 学习了 pid 表示唯一一个进程 ; 此处我们的端口号也是唯一表示一个进程 .只不过端口号用在网络通讯中。 这其实是一种解耦操作防止一方出问题另一方也造成的功能崩塌。 pid是进程管理的模块纳入到网络板块不符合低耦合 万一进程版块出错那么就会出现牵一发而动全身的效果 理解源端口号和目的端口号 传输层协议 (TCP 和 UDP) 的数据段中有两个端口号 , 分别叫做源端口号和目的端口号 . 就是在描述 数据是谁发的 , 要发给谁 ; 举个例子 玩抖音客户端---服务器 端口号c客户端 s服务器 基于ip端口的通信方式称为socket 进程如何绑定端口号呢 通过哈希运算把pcb绑定到特定的哈希表中 认识TCP协议 此处我们先对 TCP(Transmission Control Protocol 传输控制协议 ) 有一个直观的认识 ; 后面我们再详细讨论 TCP的一些细节问题 . 传输层协议 有连接 可靠传输 面向字节流 认识UDP协议 此处我们也是对 UDP(User Datagram Protocol 用户数据报协议 ) 有一个直观的认识 ; 后面再详细讨论 . 传输层协议 无连接 不可靠传输 面向数据报 网络字节序 我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢? 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出; 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存; 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节. 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据; 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可; 小小小小权值位放在小地址处为小端 为使网络程序具有可移植性 , 使同样的 C 代码在大端和小端计算机上编译后都能正常运行 ,可以调用以下库函数做网络字节序和主机字节序的转换。 对于port需要将主机序转为网络字节序就需要使用htons接口 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回 ; 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回 socket编程接口 基于ip端口当通信方式称为socket套接字 套接字Socket是计算机网络通信中一个抽象层它提供了进程间通信的端点。在网络编程中套接字可以被看作是不同计算机进程间通信的一个虚拟端点允许数据通过计算机网络进行传输。 以下是套接字的定义 套接字在计算机网络中套接字是一个软件抽象层它代表了一个网络连接的一端。每个套接字都有唯一的标识由一个IP地址和一个端口号组成。套接字使得应用程序可以发送或接收数据而不需要了解底层网络协议的细节。 套接字分为以下几种类型 流套接字Stream Sockets提供面向连接、可靠的数据传输服务通常使用TCP传输控制协议来实现。适用于需要数据完整性和顺序保证的应用如Web浏览器和电子邮件服务器。 数据报套接字Datagram Sockets提供无连接的数据传输服务通常使用UDP用户数据报协议来实现。适用于不需要可靠传输的应用如视频会议或在线游戏它们可以容忍一定的数据丢失。 原始套接字Raw Sockets允许直接发送和接收IP协议数据包通常用于特殊用途如网络诊断工具或实现新的协议。 套接字的类型 流套接字SOCK_STREAM提供可靠的、面向连接的服务通常基于TCP协议。 数据报套接字SOCK_DGRAM提供不可靠的、无连接的服务通常基于UDP协议。 原始套接字SOCK_RAW允许直接访问网络层协议如IP或ICMP。 套接字的地址家族 AF_INET用于IPv4网络协议。 AF_INET6用于IPv6网络协议。 AF_UNIX用于Unix域套接字用于同一主机上的进程间通信。 求同存异 可以看到调用接口的时候必须强转成const struct sockaddr*的结构但是不同协议簇使用的套接字是不同的那怎么做到统一呢 他们前16位是一样的大小可以确定协议簇的类型只需要强转之后取前16字节就可以获得不同的结构。 socket 常见API // 创建 socket 文件描述符 (TCP/UDP, 客户端 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
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. 然而 , 各种网络协议的地址格式并不相同。 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_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数; sockaddr 结构 sockaddr_in 结构 虽然 socket api 的接口是 sockaddr, 但是我们真正在基于 IPv4 编程时 , 使用的数据结构是 sockaddr_in; 这个结构里主要有三部分信息 : 地址类型, 端口号, IP地址. 在这个结构中我们核心关注的是前三个字段 in_addr 结构 in_addr 用来表示一个 IPv4 的 IP 地址 . 其实就是一个 32 位的整数 ; UDP套接字编程 服务端
#include iostream
#include string
#include strings.h
#include cstring
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include functional
#include Log.hppusing namespace std;using func_t std::functionstd::string(const std::string); //using xxx 类型是C风格的typede
//typedef std::functionstd::string(const std::string) func_t;Log lg;enum{SOCKET_ERR1,BIND_ERR
};uint16_t defaultport 8080; //端口号是一个16位无符号整数
uint32_t defaultip INADDR_ANY; //ip地址是一个32位无符号整数 #define INADDR_ANY ((in_addr_t) 0x00000000)
const int sz 1024;class UdpServer
{
public:UdpServer(uint16_t port defaultport, string ip to_string(defaultip)) //运行的时候只需要知道端口号和ip即可: _sockfd(0), _ip(ip), _port(port), _isrunning(false){bzero(_local, sizeof(_local));_local.sin_family AF_INET; //网络层协议族为IPv4_local.sin_addr.s_addr inet_addr(_ip.c_str()); //将ip字符串转换为网络字节序的32位整数_local.sin_port htons(_port); //将端口号转换为网络字节序的16位整数/* local.sin_family AF_INET;local.sin_port htons(port_); //需要保证我的端口号是网络字节序列因为该端口号是要给对方发送的local.sin_addr.s_addr inet_addr(ip_.c_str()); //1. string - uint32_t 2. uint32_t必须是网络序列的 // ??// local.sin_addr.s_addr htonl(INADDR_ANY);/*单网络接口如果你的服务器只有一个网络接口设置 INADDR_ANY 意味着无论客户端通过哪个IP地址连接到这个服务器服务端都会接受连接。多网络接口如果你的服务器有多个网络接口每个接口有不同的IP地址设置 INADDR_ANY 则意味着无论客户端连接到哪个IP地址服务端都会接受连接。例如服务器可能有一个IP地址用于内部网络另一个IP地址用于外部网络。多IP地址如果服务器的一个网络接口配置了多个IP地址比如通过虚拟接口或别名设置 INADDR_ANY 允许服务端在这些所有IP地址上接受连接。也就是说如果这个服务器存在多个ip那么客户端与服务器通信时可以接入本服务器的任意一个ip*///将peer暂时初始化后续会获得peer的地址信息bzero(_peer, sizeof(_peer));_peer.sin_family AF_INET;_peer.sin_addr.s_addr INADDR_ANY;_peer.sin_port htons(0);}~UdpServer(){if (_sockfd) close(_sockfd);}public:void Init(){//1.创建UDP套接字_sockfd socket(AF_INET,SOCK_DGRAM, 0); //int socket(int domain, int type, int protocol);if (_sockfd 0){lg(Fatal, socket create error, sockfd: %d, _sockfd);exit(SOCKET_ERR);}lg(Info, socket create success, sockfd: %d, _sockfd);//绑定端口号和ip地址if (bind(_sockfd, (const struct sockaddr*)_local, sizeof(_local)) 0){lg(Fatal, bind error, sockfd: %d, _sockfd);exit(BIND_ERR);}lg(Info, bind success, sockfd: %d, ip: %s, port: %d, _sockfd, _ip.c_str(), _port);}//对代码进行分层void Run(func_t func) //这个地方需要传入一个函数指针{_isrunning true;char buffer[sz] {0};while (_isrunning){socklen_t len sizeof(_peer);cout recv not over endl;ssize_t n recvfrom(_sockfd, buffer, sz, 0, (struct sockaddr*)_peer, len);if (n 0){lg(Warning, recvfrom error, errno: %d, err string: %s, errno, strerror(errno));continue; //出错了继续接收下一个数据包}cout recv over endl;buffer[n] 0;string data buffer;string echo func(data);sendto(_sockfd, echo.c_str(), echo.size(), 0, (const struct sockaddr*)_peer, len);}}private:int _sockfd; //套接字描述符,一切皆文件string _ip; //我们使用的ip一般是字符串样式uint16_t _port;bool _isrunning;struct sockaddr_in _local; struct sockaddr_in _peer;
};#include UdpServer.hpp
#include memory
#include cstdiousing namespace std;void Usage(std::string proc)
{std::cout \n\rUsage: proc port[1024]\n std::endl;
}string ExcuteCommand(const string cmd)
{FILE *fp popen(cmd.c_str(), r);if(nullptr fp){perror(popen);return error;}std::string result;char buffer[4096];while(true){char *ok fgets(buffer, sizeof(buffer), fp);if(ok nullptr) break;result buffer;}pclose(fp);return result;
}std::string Handler(const std::string str)
{std::string res Server get a message: ;res str;std::cout res std::endl;return res;
}// ./udpserver port
int main(int argc, const char* argv[])
{if(argc ! 2){Usage(argv[0]);exit(0);}uint16_t port stoi(argv[1]);unique_ptrUdpServer svr make_uniqueUdpServer(port);svr-Init();svr-Run(Handler);return 0;
} 需要注意的是 1.在对sockaddr_in成员进行初始化时需要满足网络通讯的要求1.端口号为网络字节序 2.ip地址从字符串风格编程点分式 2.recvfrom和sendto面向的是udp通信方式传参的时候需要将sockaddr_in结构进行强转。 3.核心为1.创建套接字 2.绑定端口号 3.接受发送 创建套接字 绑定端口ip时sockaddr_in初始化时端口号一般ip采用通用ip。 单网络接口如果你的服务器只有一个网络接口设置 INADDR_ANY 意味着无论客户端通过哪个IP地址连接到这个服务器服务端都会接受连接。 多网络接口如果你的服务器有多个网络接口每个接口有不同的IP地址设置 INADDR_ANY 则意味着无论客户端连接到哪个IP地址服务端都会接受连接。 例如服务器可能有一个IP地址用于内部网络另一个IP地址用于外部网络。 多IP地址如果服务器的一个网络接口配置了多个IP地址比如通过虚拟接口或别名设置 INADDR_ANY 允许服务端在这些所有IP地址上接受连接。 也就是说如果这个服务器存在多个ip那么客户端与服务器通信时可以接入本服务器的任意一个ip。 在计算机网络中存在一些端口号范围被操作系统保留用于特定的服务或进程。通常这些端口号被称为“知名端口”Well-known ports它们的范围是从0到1023。 端口是一个16位的数字可供选择。 4.用socket接口返回的套接字本质是一个文件描述符不用的时候应该关掉sockfd。 5.popen接口 参数1传入需要执行的命令 作用 在内部自动fork让父子进程建立管道让子进程执行命令将子进程的执行结果通过管道返回给调用方。 调用方想得到command的执行结果可以用FILE*文件指针的方式读取 参数二执行结果的打开方式把这个命令当成一个文件 客户端 #include iostream
#include cstdlib
#include unistd.h
#include strings.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.husing namespace std;void Usage(std::string proc)
{std::cout \n\rUsage: proc serverip serverport\n std::endl;
}// ./udpclient serverip serverportint main(int argc, const char *argv[])
{if (argc ! 3){Usage(argv[0]);exit(0);}string ip argv[1];uint16_t port (uint16_t)stoi(argv[2]);struct sockaddr_in serveraddr; serveraddr.sin_family AF_INET;serveraddr.sin_addr.s_addr inet_addr(ip.c_str());serveraddr.sin_port htons(port);int sockfd socket(AF_INET, SOCK_DGRAM, 0);if (sockfd 0){cout create socket error endl;return -1;}// client 要bind吗要只不过不需要用户显示的bind一般有OS自由随机选择// 一个端口号只能被一个进程bind对server是如此对于client也是如此// 其实client的port是多少其实不重要只要能保证主机上的唯一性就可以// 系统什么时候给我bind呢首次发送数据的时候string msg;char buffer[1024];while (1){cout Please Enter ;getline(cin ,msg);sendto(sockfd, msg.c_str(), msg.size(), 0, (const struct sockaddr*)serveraddr, sizeof(serveraddr));cout sendto over endl;struct sockaddr_in tmp;socklen_t len sizeof(tmp);ssize_t n recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)tmp, len); //udp的数据报(数据传输模式)类型的数据需要用recvfrom读取tcp的字节流可以用read读取if(n 0){buffer[n] 0;cout buffer endl;}}close(sockfd); return 0;
} 客户端不需要手动绑定端口号只需要OS去绑定即可。 客户端需要对服务端的sockaddr_in结构合理的初始化才能接收和发送到正确的Server。 TCP与UDP传输的区别 字节流Byte Stream和数据报Datagram是网络通信中两种不同的传输方式它们在数据传输的可靠性、传输方式和用途上有所区别 可靠性 字节流通常指的是面向连接的传输方式如TCP传输控制协议。它提供了可靠的数据传输确保数据按照发送顺序到达且不会丢失或重复。 数据报通常指的是无连接的传输方式如UDP用户数据报协议。它不保证数据的可靠传输数据包可能会丢失、重复或到达顺序错乱。 传输方式 字节流在字节流传输中数据像水流一样连续传输。发送方和接收方之间存在一个持续的连接数据按照顺序到达。 数据报数据报传输将数据分割成小的、独立的数据包进行发送。每个数据包携带目的地址信息但不保证按照顺序到达也不保证所有数据包都能到达。 用途 字节流适用于需要高可靠性的应用如网页浏览、文件传输、电子邮件等这些应用需要确保数据的完整性和顺序。 数据报适用于对实时性要求高但可以容忍一定丢包率的场景如视频会议、在线游戏等。这些应用更关注数据的快速传输而不是完整性。 头部开销 字节流由于需要维护连接状态通常头部开销较大因为TCP头部包含了序列号、确认号、窗口大小等信息。 数据报UDP头部相对简单开销较小只包含少量的控制信息。 速度 字节流由于其可靠性机制如重传机制可能会影响传输速度。 数据报因为没有复杂的可靠性保证机制通常传输速度较快。 在选择字节流还是数据报时需要根据应用的具体需求来决定。如果数据完整性和顺序至关重要应该选择字节流如果实时性更重要能够容忍一定的数据丢失那么数据报可能是更好的选择。 telnet不能链接udp服务因为telnet底层采用的是tcp的协议。 linux相关的指令操作 netstat可以查看本地的服务器 telnet 127.0.0.1 本地环回连接服务器常用于测试服务器是否存在BUG。 ip地址转化函数 本节只介绍基于 IPv4 的 socket 网络编程 ,sockaddr_in 中的成员 struct in_addr sin_addr 表示 32 位 的 IP 地址但是我们通常用点分十进制的字符串表示 IP 地址 , 以下函数可以在字符串表示 和 in_addr 表示之间转换 ; 字符串转in_addr的函数: aton的第二个参数 in_addr转字符串的函数: 其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。 关于 inet_ntoa inet_ntoa 这个函数返回了一个 char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果 . 那么是否需要调用者手动释放呢 ? man 手册上说 , inet_ntoa 函数 , 是把这个返回结果放到了静态存储区 . 这个时候不需要我们手动进行释放.那么问题来了 , 如果我们调用多次这个函数 , 会有什么样的效果呢 ? 参见如下代码 运行结果如下 : 因为 inet_ntoa 把结果放到自己内部的一个静态存储区 , 这样第二次调用时的结果会覆盖掉上一次的结果 . 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢? 在APUE中, 明确提出inet_ntoa不是线程安全的函数; 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁; 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;