IP地址(公网IP),标定了主机的唯一性。
通常情况,把数据送到对方的机器是目的吗?
不是的,真正的网络通信过程其实是进程间通信,如客户端进程和服务器进程,我们把数据在主机间转发仅仅是手段,机器收到数据之后,需要将数据交付给指定的进程,当客户端有多个进程在运行时,OS又是如何把数据传送给指定进程的?这个跟端口号有关。
认识端口号
端口号(port)是传输层协议的内容
端口号是一个2字节16位的整数;
端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
一个端口号只能被一个进程占用
端口号是标识特定主机上的网络进程的唯一性。
这里80和90分别是俩台机器服务进程
![](https://img-blog.csdnimg.cn/img_convert/a0adb5857984691e7c7aced4e77fb6b3.png)
任何一个发出的报文都要包含IP和端口号,IP找目标主机,端口找目标进程。
端口号和进程ID没有任何的关系。但俩者都具有唯一性。
另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。
IP地址+端口号,我们一般称之为套接字,网络通信的本质就是进程间通信。
初识TCP/UDP协议
传输层是离操作系统最近的,应用层用的接口一般是TCP/UDP提供的,传输层离应用层最近,所以我们一般使用TCP/UDP接口。TCP/UDP都是传输层协议。
UDP也叫做用户数据报协议,特点:无连接(写代码时不用刻意建立连接,可直接发送数据)、不可靠传输(可能会出现网络丢包的问题)、面向数据报。
TCP也叫做传输控制协议,特点:有连接、可靠传输、面向字节流。
网络字节序
俩台机器在进行数据传输时,中间要经过网络,而俩台机器在存储数据时,可能会出现俩种不同的存储方式如A机器大端存储,B机器小端存储,这样就可能会出现一个问题,导致收到的数据在进行存储时会出现字节序混乱,因此网络规定,所有网络数据,都必须是大端存储。
即A如果要发来小端,网络是不允许接收的,则需要A转换成大端再发送数据。
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可
一般情况下,我们不用考虑大小端的问题,直接进行数据的收发即可。
![](https://img-blog.csdnimg.cn/img_convert/49fcac6d78688b078d4e054a496b835c.png)
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
hton开头的:主机转网络,即无论主机是什么序列都转成大端。
ntoh开头的:网络转主机。
结尾的l:转为4字节数据。 s:2字节数据。
![](https://img-blog.csdnimg.cn/img_convert/f7740b733b0c8ba1c189de6237bc9f54.png)
socket(套接字)编程接口
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);
这些函数里都有一个struct sockaddr *
常见的套接字:1.域间套接字 2.原始套接字 3.网络套接字。理论上这些是三种应用场景,对应的应该是三套接口,但Linux设计的时候不想设计过多的接口,所以将所有的接口进行统一。
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同。
![](https://img-blog.csdnimg.cn/img_convert/f5241795379c4ccf518333062e6e27c7.png)
如果是网络套接字,我们实际上传递给套接字对应的参数或内容,必须是端口号和IP地址。给套接字相关函数传递的参数必须是struct sockaddr-in这个结构,因为这个结构包含端口号和IP地址。
域间套接进行本地通信,一般用struct sockaddr_un这样的结构体。
struct sockaddr是一个通用接口。
这三个接口的前俩个字节,都表示地址的类型,当传入第一个接口之后,对前俩个字节做判断,如果前俩个字节如果是AF_INET说明是网络套接字,就按照网络的处理方式处理,如果是AF_UNIX就说明是域间套接字,就按本地通信进行处理。
第一个类型就相当于基类,后面俩个都是它的子类。以后在传递参数时,只需要传第一个接口,至于最后按照哪种方式处理,完全取决于前俩个字节。
socket函数
![](https://img-blog.csdnimg.cn/img_convert/9c7a1765a0c8ef4b92f21bd8ac1ae268.png)
套接字创建成功返回文件描述符。
第一个参数表示套接字的域(表明是哪种类型的套接字),通过这个参数可说明当前通信是网络通信还是本地通信。
AF_INET表示进行网络通信
套接字的类型
![](https://img-blog.csdnimg.cn/img_convert/b1d9b4cd5f3b650cf101edbf48c3d6ee.png)
第二个参数是类型,即通信种类。下面是通信种类。这个参数说明在第一个参数的基础上以哪种方式通信,如第一个参数是网络通信,这个参数就是以流式或数据报的方式通信。
![](https://img-blog.csdnimg.cn/img_convert/59ffea4c76ed79867cdf7ac7fffdde5d.png)
第三个参数由前俩个参数来确定,这个参数我们一般忽略即可,一般写成0。
bind绑定函数
![](https://img-blog.csdnimg.cn/img_convert/7c3a303ab442a1f30a256de6286a50b7.png)
如果当前创建好了套接字,有了相应的文件描述符,未来通信就可以通过文件描述符进行IO,但套接字通信是俩台主机上的应用跨网络在通信,我们需要用IP和端口来标识唯一性,我们写到这里只是创建了套接字,但还未告诉OS ip和端口是多少。
因此我们就需要进行绑定,即将用户设置的ip和port在内核中和我们当前的进程强关联,因此就用bind函数,即将ip,端口号根进程关联起来。
![](https://img-blog.csdnimg.cn/img_convert/74781fdcbb3a9dc1e185c78168b63823.png)
返回值,成功返回0,失败返回-1.
第一个是套接字,第二个参数是填充IP和端口信息的结构体
sockaddr* addr
第二个参数还需要这些头文件
![](https://img-blog.csdnimg.cn/img_convert/b66e879b706935669d259d14e04b6684.png)
第二个参数结构体里填充的是端口号,ip地址,还有一些其它数据。
”192.168.1.1“这种ip地址被称作点分十进制字符串风格的IP地址
以点作为分隔符的每一个区域取值范围是[0-255],有8个比特位即一个字节,也就是把一个字节划分成了4个区域,理论上要表示一个IP地址,其实4个字节就够了。
如果是这样”192.168.111.111“这种写法有十几个字节,网络用的是4字节IP,192.168.111.111“这种是给用户去看的,因为可读性强,因此我们要进行互相转换
点分十进制字符串风格的IP地址<->4字节。下面的这些接口,就可以帮我们实现这种转换。
![](https://img-blog.csdnimg.cn/img_convert/0041d9a800d7eeb5323f22789bc64389.png)
结构体里面还有一个struct in_addr sin_addr这个表示IP地址,实际上这个结构体是经过了好几次封装的整数。
unsigned char sin_zero我们一般进行清0即可
![](https://img-blog.csdnimg.cn/img_convert/1e93d76927929bc9b255d4bcb01355a9.png)
我们在使用的时候一般对sockaddr_in进行清0,出国memset之外还有bzero这个接口,即在指定字节数的内存空间中,将数据全部进行清0。
![](https://img-blog.csdnimg.cn/img_convert/fb31caea9b2215f6fee3437c61516c90.png)
recvfrom
该接口是读取数据的接口
![](https://img-blog.csdnimg.cn/img_convert/fd55b90a05852bfc7ea14bb8a1c1bd4d.png)
第一个参数套接字,第二个,第三个参数,缓冲区,缓冲区长度,当读取到数据后要把数据放到缓冲区中,flags读取方式,默认以阻塞方式读取,最后俩个参数是输出型参数,除了拿到数据以外,我们也想知道数据是谁发送的,我们需要传入特定格式的结构体和结构体信息,方便我们把对方主机的ip和端口提取出来。
返回值读取成功,返回读取到的字节数,失败返回-1
我们接下来写一个简单的服务器,client客户端发送消息,我们原封不动返回
sendto
sendto是用来返回消息的
![](https://img-blog.csdnimg.cn/img_convert/bf1df6fa62fb0077a4750e457da01a4a.png)
第一个参数文件描述符,第二个要发送的数据,第三个要发送数据的长度,flag默认为0,后俩个参数含义是把数据要发给谁,以及对方缓冲区长度是多少。
netstat
用来查看本地主机的服务器的启动情况和服务器和多少主机关联,netstat -nup
![](https://img-blog.csdnimg.cn/img_convert/66beba5342a38112ee4e2af1a099c6f0.png)
我们程序写好启动服务器的时候,输入的这个IP地址叫本地环回.
本地环回:client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中。本地回环通常用来进行本地网络服务器的测试。
![](https://img-blog.csdnimg.cn/img_convert/c31bff884a3423ad337ade915dacf2ca.png)
由于客户端和服务器在一台机器上。数据只会把数据栈走一走,不经过网络层。
![](https://img-blog.csdnimg.cn/img_convert/c5e26ce613ccdafc4235b13a1b2fd7bc.png)
![](https://img-blog.csdnimg.cn/img_convert/e88ce6e146d18482749f69b2eebb9ffa.png)
我们可看到端口号是53476,client自动绑定的端口号是53476,服务端绑的是8080,我们自动设定的IP和端口号在发送消息时,自动和OS绑定
![](https://img-blog.csdnimg.cn/img_convert/7009d224980992577352e67eb9fc951b.png)
当我们用云服务器公网IP时会出错
![](https://img-blog.csdnimg.cn/img_convert/b7d56a1b46be3c0c7e017791b1614af5.png)
云服务器:1.无法绑定(bind)公网IP,也不建议,作为服务器来讲,我们也不推荐绑定确定的ip,我们推荐使用任意IP的方案。
因此,我们修改程序不让绑定输入的IP,我们在服务端运行的时候就不输入IP地址了,只需要输入端口。
INADDR_ANY这个宏就是0,让服务器在工作过程中可以从任意IP中获取数据,凡是发给这台主机上的指定端口的所有数据,都可以获取到。
![](https://img-blog.csdnimg.cn/img_convert/6ec9f04efd1375fb8cbc4d3e95afa5ad.png)
此时IP地址就是全0
popen
![](https://img-blog.csdnimg.cn/img_convert/891f48040ad2fff2b2a330d7a183009e.png)
该接口会执行字符串,先创建管道,再fork,让子进程调用exec系列接口执行command命令,可以将执行结果通过FILE*进行读取。
代码
log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>
// 日志是有日志级别的
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4
const char *gLevelMap[] = {
"DEBUG",
"NORMAL",
"WARNING",
"ERROR",
"FATAL"
};
#define LOGFILE "./threadpool.log"
// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
if(level== DEBUG) return;
#endif
// va_list ap;
// va_start(ap, format);
// while()
// int x = va_arg(ap, int);
// va_end(ap); //ap=nullptr
char stdBuffer[1024]; //标准部分
time_t timestamp = time(nullptr);
// struct tm *localtime = localtime(×tamp);
snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%ld] ", gLevelMap[level], timestamp);
char logBuffer[1024]; //自定义部分
va_list args;
va_start(args, format);
// vprintf(format, args);
vsnprintf(logBuffer, sizeof logBuffer, format, args);
va_end(args);
//FILE *fp = fopen(LOGFILE, "a");
printf("%s%s\n", stdBuffer, logBuffer);
//fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
//fclose(fp);
}
makefile
.PHONY:all
aLL:udp_client udp_server
udp_client:udp_client.cc
g++ -o $@ $^ -std=c++11
udo_server:udp_server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f udp_client udp_server
udp_client.cc
#include<iostream>
#include<string>
#include<sys/socket.h>
#include<sys/types.h>
#include<cstring>
#include<arpa/inet.h>
#include<netinet/in.h>
static void usage(std::string proc)
{
std::cout<<"\nUsage:"<<proc<<" serverIp serverPort\n"<<std::endl;
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
usage(argv[0]);
exit(1);
}
int sock=socket(AF_INET,SOCK_DGRAM,0);//创建套接字
if(sock<0)
{
std::cout<<"socket error"<<std::endl;
exit(2);
}
//client(客户端)要不要绑定?需要,但clietn一般不会显示的bind,程序员不会自己bind
//client是一个客户端->普通人下载安装启动使用的-。如果程序员自己bind了
//client一定bind了一个固定的ip和port,万一,其它客户端(进程)提前占用了这个port呢?这个时候我们固定绑定的(客户端)进程就无法启动
//client一般不需要显示的bind指定port,而是让OS自动随机选择
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
char buffer[1024];//存储从服务器上接收到的内容
while(true)
{
std::cout<<"请输入你的信息# ";
std::string message;
std::getline(std::cin,message);
//客户端本身也有自己的IP和端口
//当clietn首次发送消息给服务器的时候,OS会自动随机给client bind它的IP和port
struct sockaddr_in temp;//临时占位用的
socklen_t len=sizeof(temp);//临时站函数位置用来,无实际意义
sendto(sock,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof server);//client发送消息给服务器
ssize_t s=recvfrom(sock,buffer,sizeof buffer,0,(struct sockaddr*)&temp,&len);//读取数据,从sock将数据读到buffer中
if(s>0)
{
buffer[s]=0;
std::cout<<"server echo# "<<buffer<<std::endl;
}
}
return 0;
}
udp_server.cc
#include"udp_server.hpp"
#include<memory>
#include<cstdlib>
static void usage(std::string proc)//打印用户手册
{
std::cout<<"\nUsage:"<<proc<<" ip port\n"<<std::endl;
}
//我们后面在运行服务器的时候要输入./udp_server ip port,因此argc要是3
//要根据ip和port运行
int main(int argc,char *argv[])
{
if(argc!=2)
{
usage(argv[0]);//如果传入的命令行个数不是3,我们就把用户输入命令的使用手册打出来
exit(1);
}
//std::string ip=argv[1];
uint16_t port=atoi(argv[1]);//argv是char*数组,即传来的是个字符串,我们将其转为整形
std::unique_ptr<UdpServer> svr(new UdpServer(port,ip));//智能指针
svr->initServer();//先初始化
svr->Start();//再启动服务器
return 0;
}
udp_server.hpp
#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP
#include<iostream>
#include<string>
#include<strings.h>
#include<cstdio>
#include"log.hpp"
#include<vector>
#include<cerrno>
#include<stdint.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<cstdlib>
#define SIZE 1024
class UdpServer
{
public:
UdpServer(uint16_t port,std::string ip="")
:_port(port),_ip(ip),_sock(-1)
{}
bool initServer()//初始化服务器
{
//从这里开始,就是新的系统调用,来完成网络功能
//1.创建套接字
_sock=socket(AF_INET,SOCK_DGRAM,0);
if(_sock<0)//套接字创建失败,打印错误信息
{
logMessage(FATAL,"%d:%s",errno,strerror(errno));
//strerror是把错误码转换成错误信息
exit(2);
}
//2.bind绑定将IP和端口号绑定
struct sockaddr_in local;//闯将一个结构体
bezro(&local,sizeof(local));//进行清0操作
local.sin_family=AF_INET;//sin-family一般也填socket的第一个参数,这里我们用的是网络通信
//服务器的IP和端口,未来也是要发送给对方主机的,先要将数据发送到网络
local.sin_port=htons(_port);//填入端口号,要主机序列转网络序列,保持大端.
//1.同上,先要将点分十进制字符串风格的IP地址->4字节IP
//2.4字节主机序列->网络序列
//有一套接口,可以一次帮我们做完这俩件事情
local.sin_addr.s_addr=_ip.empty()?INADDR_ANY:inet_addr(_ip.c_str());//INTADDR_ANY这个宏就是0,含义时让服务器在工作过程中,可以从任意IP中获取数据。
if(bind(_sock,(struct sockaddr*)&local,sizeof(local)<0))
{
logMessage(FATAL,"%d:%s",errno,strerror(errno));
exit(2);
}
logMessage(NORMAL,"init udp server done ... %s ",strerror(errno));
}
void start()//启动服务器
{
//作为一款网络服务器,该服务器永远不退出。
//该服务器就是进程,,也就是常驻进程,永远在内存中,除非挂了!
char buffer[SIZE];//读取缓冲区
//这个结构体获取对方主机的信息
for(;;)
{
//如果peer是纯输出型参数
//len就是输入输出型
//输出:peer缓冲区大小
//输出:实际读到的peer
struct sockaddr_in peer;
bzero(&peer,sizeof(peer));//peer设置为0
socklen_t len=sizeof(peer);
//1.读取数据
ssize_t s=recvfrom(_sock,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(s>0)//如果读取数据成功
{
buffer[s]=0;//目前数据当作字符串
//输出发送数据信息
//谁发送的
uint16_t cli_port=ntohs(peer.sin_port);//拿到客户端端口号,这是从网络中拿出来的,因此我们要从网络序列转为主机序列
std::string cli_ip=inet_ntoa(perr.sin_addr);//4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
printf("[%s:%d#%s\n]",cli_ip.c_str(),cli_port,buffer);
}
//分析和处理数据
sendto(_sock,buffer,strlen(buffer),0,(struct sockaddr*)&peer,len);
2.协会数据
}
}
~UdpServer()
{
if(_sock>=0) close(_sock);
}
//一个服务器必须有IP地址,端口号port(一般是16位整数)
private:
std::string _ip;
uint16_t _port;
int _sock;//表示通信套接字,本质是文件描述符
};
#endif
无论是读还是写,用的sock都是一个,sock代表文件,UDP是全双工的,即可以同时进行收发而不受干扰。