35人参与 • 2025-08-11 • Linux
因特网是在网络级进行互联的,因此,因特网在网络层(ip 层)完成地址的统一工作,把不同物理网络的地址统一到具有全球惟一性的 ip地址上,ip 层所用到的地址叫作因特网地址,又叫 ip 地址。ip 地址的意义就是标识公网内唯一一台主机。
在 ip 数据包头部中 有两个 ip 地址, 分别叫做源 ip 地址 和目的 ip 地址。
如果我们的台式机或者笔记本没有 ip 地址就无法上网,而因为每台主机都有 ip 地址,所以注定了数据从一台主机传输到另一台主机就一定有源 ip 地址和目的 ip 地址,所以在报头中就会包含源ip 地址和目的 ip 地址。
网络通信的本质是进程间通信,有了 ip 就可以标识公网内唯一的一台主机,想要完成网络通信我们还需要一个东西来标识一台主机上的某个进程,这个标识就是端口号(port)。
端口号是传输层协议的内容,它包括如下几个特点:
ip 地址(标识唯一主机)+ 端口号(标识唯一进程)能够标识网络上的某一台主机的某一个进程(全网唯一的进程)
端口号的解释:
每个端口号都有特定的作用和用途,例如常见的端口号有:
既然 pid 已经做到唯一标识一个进程,为何还要引入端口号呢?
我们可以从生活的角度去理解这种情况:即然每个人都有了唯一标识自己的身份照号,为何学校还要给我们分配学号呢?直接用身份照号不行吗?
在学校我们用学号,相比于身份证更简便,假如我的学号是2211211023,这样就能看到我是22级的,方便阅读信息。可是出了学校,别人并不能通过学号辨别你。也许你在不同的学习有不同的学号。pid和端口号也是一样的。
虽然一个端口号只能绑定一个进程,但是一个进程可以绑定多个端口号。前面说了有源 ip 和目的 ip,而这里的 port 也有源端口号和目的端口号。我们在发送数据的时候也要把自己的 ip 和端口号发送过去,因为数据还要被发送回来,所以发送数据时一定会多出一部分数据(以协议的形式呈现)。
socket 通信的本质就是跨网络的进程间通信,任何的网络客户端和网络服务如果要进行正常的数据通信,它们必须要有自己的端口号和匹配所属主机的 ip 地址。
我们进行网络编程时通常是在应用层编码,应用层下面就是传输层。应用层往下传输数据时不必担心也没有必要知道数据的传输情况如何,这个具体地交给传输层来解决,所以我们有必要简单了解一下传输层的两个重要协议 tcp 和 udp。
tcp (transmission control protocol 传输控制协议)的特点:
udp 全称 user datagram protocol,即用户数据报协议,它有如下特点:
在我们的认知里一定是安全、稳定的才好,那传输层为什么还要引入一个不可靠传输方式的 udp 协议呢?tcp 协议虽然是可靠传输,但是“可靠”是要付出一些效率上的代价的,可能会导致传输速度比较慢,而且实现起来相对复杂;以这个角度去看 udp 协议,虽然可能在传输过程中出现丢包的情况,但效率上是要比 tcp 更快的。通常两个协议我们可以搭配起来使用,网速快时用 tcp 协议,网速慢时用 udp 协议,但如果是要传输重要数据的话就应该用 tcp 了。
我们知道,内存中的数据权值排列相对于内存地址的大小有大端和小端之分:
数据在发送时,发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序以字节为单位发出接收主机把接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序以字节为单位保存的。即先发出低地址的数据,后发出高地址的数据;接收到的数据也是按低地址到高地址的顺序接收。
如果发送端和接收端主机的存储字节序不同,则会造成发送的数据和识别出来的数据不一致的问题,如下图所示:
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。tcp/ip 协议规定:网络数据流应采用大端字节序,即低地址高字节。不管这台主机是大端机还是小端机,都会按照这个 tcp/ip 规定的网络字节序来发送/接收数据。如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可。
为使网络程序具有可移植性,使同样的 c 代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
socket 通常也称为“套接字”,程序可以通过“套接字”向网络发出请求或者响应网络请求。socket 位于传输层之上、应用层之下。socket 编程是通过一系列系统调用完成应用层协议,如 ftp、telent、http 等应用层协议都是通过 socket 编程来实现的。
从套接字所处的位置来讲,套接字上连应用进程,下接网络协议栈,是应用程序与网络协议栈进行交互的接口。
套接字可以看作是通信的两个端点,一个是服务器端的套接字,另一个是客户端的套接字。通过套接字,服务器端和客户端可以相互发送和接收数据。
在网络通信中,套接字使用网络协议(如 tcp/ip、udp 等)来完成数据的传输和通信。根据所使用的网络协议的不同,套接字可以分为两种类型:
linux 和 unix 的 i/o 内涵是系统中的一切都是文件。当程序在执行任何形式的 i/o 时,程序都是在读或者在写一个文件描述符,从而实现操作文件,但是,这个文件可能是一个 socket 网络连接、目录、fifo、管道、终端、外设、磁盘上的文件。一样的道理,socket 也是使用标准 linux 文件描述符和其他网络进程进行通信的。
socket 函数基本为系统调用函数,它是操作系统向网络通信进程提供的函数接口。
在tcp/ip协议中, 用 “源ip”, “源端口号”, “目的ip”, “目的端口号”, “协议号” 这样一个五元组来标识一个网络通信,我们可以用 netstat -n 命令查看当前主机下已经建立链接的网络通信
ip地址、端口号、socket 套接字三者在数据结构上的联系
socket
创建 socket 文件描述符(tcp/udp, 客户端 + 服务器)
bind
绑定端口号( tcp/udp, 服务器)
listen
开始监听 socket(tcp, 服务器)
accept
接收请求( tcp, 服务器)
connect
建立连接( tcp, 客户端)
socket api 是一层抽象的网络编程接口,适用于各种底层网络协议,如:ipv4、ipv6,以及后面要讲的 unix domain socket。然而,各种网络协议的地址格式并不相同。
套接字有不少类型,常见的有三种:
三种应用场景:网络套接字主要运用于跨主机之间的通信,也能支持本地通信,而域间套接字只能在本地通信,而原始套接字可以跨过传输层(tcp/ip 协议)访问底层的数据。
为了方便,设计者只使用了一套接口,这样就可以通过不同的参数来解决所有的通信场景。这里举两个具体的套接字类型:sockaddr_in 和 sockaddr_un:
可以看到 sockaddr_in 和 sockaddr_un 是两个不同的通信场景,区分它们就用 16 地址类型协议家族的标识符。但是,这两个结构体都不用,我们用 sockaddr。
比方说我们想用网络通信,虽然参数是 const struct sockaddr *addr,但实际传递进去的却是 sockaddr_in 结构体(注意要强制类型转换)。在函数内部一视同仁,全部看成 sockaddr 类型,然后根据前两个字节判断到底是什么通信类型然后再强转回去。可以把 sockaddr 看成基类,把 sockaddr_in 和 sockaddr_un 看成派生类,构成了多态体系。
(1)sockaddr 结构
(2)sockaddr_in 结构
虽然 socket api 的接口是 sockaddr, 但是我们真正在基于 ipv4 编程时, 使用的数据结构是 sockaddr_in, 这个结构里主要有三部分信息: 地址类型, 端口号, ip 地址。
(3)in_addr 结构
in_addr 用来表示一个 ipv4 的 ip 地址,其实就是一个 32 位的整数。
(3)ip地址转换函数
ip 地址转换函数是指完成点分十进制数 ip 地址(是一个字符串)与二进制数ip地址之间的相互转换。ip 地址转换主要由 inet_aton、inet_addr 和 inet_ntoa 这三个函数完成,但它们都只能处理 ipv4 地址,而不能处理 ipv6 地址。这三个函数的函数原型及其具体说明如下。
1、inet_addr
2、inet_aton
3、inet_ntoa
udp 协议是非连接非可靠的数据传输,常用在对数据质量要求不高的场合。udp 服务器通常是非连接的,因而,udp 服务器进程不需要像 tcp 服务器那样在监听套接字上接收新建的连接;udp 只需要在绑定的端口上等待客户机发送过来的 udp 数据报文,并对其进行处理和响应。
一个 tcp 服务进程只有在完成了对某客户机的服务后,才能为其它的客户机提供服务。而 udp 服务器只是接收数据报文,处理并返回结果。udp 支持广播和多播,如果要使用广播和多播,必须使用 udp 套接字。udp 套接字没有连接的建立和终止过程,udp 只需要两个分组来交换一个请求和答应。udp 不适合海量数据的传输。
udp服务端基本框架
udp_server.cpp
创建套接字
在通信之前要先把网卡文件打开,函数作用:打开一个文件,把文件和网卡关联起来
第一个 af_unix 表示本地通信,而 af_inet 表示网络通信
这里我们讲的是 udp,所以使用 sock_dgram。
返回值,成功返回一个新的套接字描述费用,失败返回-1错误码被设置。
接下来我们创建套接字,创建完套接字我们要bind绑定
绑定 bind
所以我们要先定义一个 sockaddr_in 结构体填充数据,再传递进去。
点分十进制字符串风格的 ip 地址(例:"192.168.110.132" )每一个区域取值范围是 [0-255]:1字节 -> 4个区域。理论上,表示一个ip地址,其实4字节就够了。点分十进制字符串风格的 ip 地址为4字节。
返回值,成功0被返回,失败-1被返回,错误码被设置
这几个参数是什么呢?
struct sockaddr_in { short int sin_family; // 地址族,一般为af_inet或pf_inet unsigned short int sin_port; // 端口号,网络字节序 struct in_addr sin_addr; // ip地址 unsigned char sin_zero[8]; // 用于填充,使sizeof(sockaddr_in)等于16 };
创建结构体后要先清空数据(初始化),我们可以用 memset,系统也提供了接口:
填充端口号的时候要注意端口号是两个字节的数据,涉及到大小端问题。
接口我们在网络字节序具体介绍了
对于 ip,首先我们要先转成整数,再解决大小端问题。系统给了直接能解决这两个问题的接口
为什么这里的ip为什么是local.sin_addr.s_addr
这里镶嵌了一个结构体
进行绑定bind
作为一款网络服务器,是永远不退出的。
服务器启动-> 进程 -> 常驻进程 -> 永远在内存中存在,除非挂了
首先要知道服务器要死循环,永远不退出,除非用户删除。站在操作系统的角度,服务器是常驻内存中的进程,而我们启动服务器的时候要传递进去 ip 和端口号。
我们要要进行网络通信,在网络基础1的时候给大家讲了,报头包含了对方的ip和端口号还包含了自己的ip和端口号。
读取数据 recvfrom
返回值
现在我们想要知道是谁发送过来的消息,信息都被保存到了 client结构体中,我们知道 ip 信息在 client.sin_addr.s_addr 中。首先这是一个网络序列,要转成主机序列,其次为了方便观察,要把它转换成点分十进制。
操作系统给了一个接口能够解决这两个问题:
inet_ntoa 这个函数返回了一个 char*,很显然是这个函数自己在内部为我们申请了一块内存来保存 ip 的结果。那么是否需要调用者手动释放呢?
man 手册上说,inet_ntoa 函数是把这个返回结果放到了静态存储区。这个时候不需要我们手动进行释放。
同样获取端口号的时候也要由网络序列转成主机序列:
4字节的网络序列要转化回本主机的字符串风格的ip
我们收到了消息,把数据发回
发回消息sendto
完整代码
udpserve.hpp
#include <iostream> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <strings.h> #include <string.h> #include <cerrno> #include "log.hpp" log lg; const std::string defaultip = "0.0.0.0"; enum { sockfderr = 1, binderr, }; class udpserver { public: udpserver(const uint16_t& port = 8080,const std::string& ip = defaultip) :_port(port) ,_ip(ip) ,sockfd(-1) {} void init() { //1.创建套接字 sockfd = socket(af_inet,sock_dgram,0); if(sockfd < 0) { lg(fatal,"sockfd create error %d", sockfd); exit(sockfderr); } lg(info,"sockfd create success %d", sockfd); //2.bind:将用户设置的的ip和port在内核中和我们当前的进程相关联 struct sockaddr_in local; bzero(&local,0); local.sin_family = af_inet; local.sin_port = htons(_port); //主机转网络 h表示host,n表示network local.sin_addr.s_addr = inet_addr(_ip.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // if(bind(sockfd,(const struct sockaddr*)&local,sizeof(local)) < 0) { lg(fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno)); exit(binderr); } lg(info,"bind success!"); } void start() { char buffer[1024]; while(true) { //纯输出型参数 struct sockaddr_in client; //清空数据 bzero(&client,0); socklen_t len = sizeof(client); ssize_t n = recvfrom( sockfd , buffer,sizeof(buffer)-1, 0 , (struct sockaddr*)&client , &len); if(n < 0) { lg(warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno)); continue; } buffer[n] = 0; uint16_t port = ntohs(client.sin_port); //client是从网络来,我们要转为主机字序 std::string ip = inet_ntoa(client.sin_addr); //4字节的网络序列要转化回本主机的字符串风格的ip sendto(sockfd, buffer, strlen(buffer), 0, (const sockaddr*)&client, len); } } ~udpserver() { if(sockfd>0) close(sockfd); } private: int sockfd; //网络套接字描述符 std::string _ip; //ip地址 uint16_t _port; //端口号 };
main.cc
#include "udpserver.hpp" #include <memory> void usage(std::string s) { std::cout << "\n\rusage: " << s << " port[1024+]\n" << std::endl; } int main(int argc, char* argv[]) { if(argc != 2) { usage(argv[0]); exit(0); } uint16_t port = std::stoi(argv[1]); udpserver* ptr = new udpserver(port); //创建一个服务器对象 ptr->init(); //初始化服务器对象 ptr->start(); //启动服务器 return 0; }
log.hpp是我们自己写的日志文件系统,在之前的博客也有讲解。
运行结果如下:
这里阻塞了,因为我们还没写客户端
这里有个补充,我们多次使用inet_ntoa这个函数时,我们先来看一段代码
因为 inet_ntoa 把结果放到自己内部的一个静态存储区,这样第二次调用时的结果会覆盖掉上一次的结果。
如果有多个线程调用 inet_ntoa,是否会出现异常情况呢?
客户端要能与服务器进行连接,就要知道服务器的ip和端口号,但这个实际是要我们自己填写的。
client 要不要 bind?
这里我们发送数据用sendto函数
这里的参数和前面讲的 recvfrom 差不多,而这里的结构体内部需要自己填充目的 ip 和目的端口号。
#include <iostream> #include <string.h> #include <string> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> void usage(const std::string& s) { std::cout << "\n\rusage: " << s << " serverip serverport\n" << std::endl; } int main(int argc , char* argv[]) { if(argc != 3) { usage(argv[0]); exit(0); } int sockfd = socket(af_inet,sock_dgram,0); if(sockfd < 0) { std::cerr << "socket error!" <<std::endl; exit(1); } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); struct sockaddr_in server; bzero(&server,0); server.sin_family = af_inet; server.sin_port = htons(serverport); server.sin_addr.s_addr = inet_addr(serverip.c_str()); char buffer[1024]; std::string message; socklen_t len = sizeof(server); while(true) { std::cout << "client say# "; std::getline(std::cin,message); //当client首次给服务器发送消息的时候,os会自动bind client的port和ip sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len); struct sockaddr_in temp; socklen_t temp_len = sizeof(temp); ssize_t n = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(sockaddr*)&temp,&temp_len); if(n > 0) { buffer[n] = 0; std::cout << "server say$ " << buffer << std::endl; } } close(sockfd); return 0; }
我们看到客户端的端口号是随机值。
这里的 127.0.0.1 叫做本地环回。client 和 server 发送数据只在本地协议栈中进行数据流动,不会将我们的数据发送到网络中。
作用:用来做本地网络服务器代码测试的,意思就是如果我们绑定的 ip 是 127.0.0.1 的话,在应用层发送的消息不会进入物理层,也就不会发送出去。
当我们运行起来后想要查看网络情况就可以用指令 netstat,后边也可以附带参数:
客户端多线程处理收到消息和发送消息
#include <iostream> #include <cstdlib> #include <unistd.h> #include <strings.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <pthread.h> #include "terminal.hpp" using namespace std; void usage(std::string proc) { std::cout << "\n\rusage: " << proc << " serverip serverport\n" << std::endl; } struct threaddata { struct sockaddr_in server; int sockfd; std::string serverip; }; void *recv_message(void *args) { // openterminal(); threaddata *td = static_cast<threaddata *>(args); char buffer[1024]; while (true) { memset(buffer, 0, sizeof(buffer)); struct sockaddr_in temp; socklen_t len = sizeof(temp); ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len); if (s > 0) { buffer[s] = 0; cerr << buffer << endl; } } } void *send_message(void *args) { threaddata *td = static_cast<threaddata *>(args); string message; socklen_t len = sizeof(td->server); std::string welcome = td->serverip; welcome += " comming..."; sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len); while (true) { cout << "please enter@ "; getline(cin, message); // std::cout << message << std::endl; // 1. 数据 2. 给谁发 sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len); } } // 多线程 // ./udpclient serverip serverport int main(int argc, char *argv[]) { if (argc != 3) { usage(argv[0]); exit(0); } std::string serverip = argv[1]; uint16_t serverport = std::stoi(argv[2]); struct threaddata td; bzero(&td.server, sizeof(td.server)); td.server.sin_family = af_inet; td.server.sin_port = htons(serverport); //? td.server.sin_addr.s_addr = inet_addr(serverip.c_str()); td.sockfd = socket(af_inet, sock_dgram, 0); if (td.sockfd < 0) { cout << "socker error" << endl; return 1; } td.serverip = serverip; pthread_t recvr, sender; pthread_create(&recvr, nullptr, recv_message, &td); pthread_create(&sender, nullptr, send_message, &td); pthread_join(recvr, nullptr); pthread_join(sender, nullptr); close(td.sockfd); return 0; }
我们看到客户端有一个主线程和两个收到发送的线程
udp(windows 环境下 c++ 实现)
在 windows 下写客户端,在 linux 下用 linux 充当服务器实现客户端发送数据,服务器接收数据的功能(windows 下的套接字和 linux 下的几乎一样)。
windows环境下的client.cpp
#define _winsock_deprecated_no_warnings 1 #include <iostream> #include <winsock2.h> #include <windows.h> #include <string> using namespace std; uint16_t port = 8080; std::string serverip = "8.155.26.31"; #pragma comment(lib, "ws2_32.lib") int main()//_tmain,要加#include <tchar.h>才能用 { wsadata wsadata; //初始化信息 word sockversion = makeword(2, 2); //启动winsock if (wsastartup(sockversion,&wsadata) != 0) { cout << "wsastartup error = " << wsagetlasterror() << endl; return 0; } else { cout << "start success" << endl; } //创建socket socket clientsocket = socket(af_inet, sock_dgram, 0); if (clientsocket == socket_error) { cout << "socket error = " << wsagetlasterror() << endl; return 1; } else { cout << "socket success" << endl; } sockaddr_in dstaddr; dstaddr.sin_family = af_inet; dstaddr.sin_port = htons(port); dstaddr.sin_addr.s_un.s_addr = inet_addr(serverip.c_str()); char buffer[1024]; while (true) { std::string message; std::cout << "client say# "; std::getline(std::cin, message); sendto(clientsocket, message.c_str(), (int)message.size(), 0, (struct sockaddr*)&dstaddr, sizeof(dstaddr)); struct sockaddr_in temp; int len = sizeof(temp); int s = recvfrom(clientsocket, buffer, 1023, 0, (struct sockaddr*)&temp, &len); if (s > 0) { buffer[s] = 0; std::cout << "server say$ " << buffer << std::endl; } } //关闭socket连接 closesocket(clientsocket); wsacleanup(); return 0; }
运行结果如下:
这里要实现正常通信,云服务器要进行被远程访问,就需要开放公网 ip 的端口
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论