本文专栏: Linux网络编程
目录
[二,Echo Server(UDP实现)](#二,Echo Server(UDP实现))
[绑定端IP和 端口号](#绑定端IP和 端口号)
一,Socket编程基础
1,IP地址和端口号
IP地址:
- 定义:IP在网络中,用来标识主机的唯一性。或者说IP地址是分配给网络设备的唯一标识,用于在互联网中定位设备。
作用:
设备寻址:确保数据包从源设备发送到目标设备
路由选择:路由器根据IP地址决定数据包的转发路径。
端口号:端口号是传输层协议的内容。
端口号是一个2字节,16为的整数。
端口号用来标识唯一进程,告诉操作系统,当前的这个数据要交给哪个进程来处理。
一个端口号只能 被一个进程占用。
所以IP地址+端口号能够标识网络上的某一台主机的某一个进程。
端口号划分范围
- 0-1023:是指明端口号,HTTP,SSH,FTP等广为使用的应用层协议,他们的端口号都是固定的。
- 1024-65535:是操作系统动态分配的端口号。客户端运行的程序,就是由操作系统从这个范围分配的。
理解端口号和进程ID
在操作系统中,我们知道pid表示唯一一个进程。而这里端口号也是表示唯一一个进程 。这两个之间有什么关系?
进程ID属于系统概念,技术上 也具有唯一性,确实可以用来标识唯一的一个进程 。但是如果让进程ID代替端口号,会让系统进程管理和网络强耦合 。
源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。
就是在 描述"数据是谁发的,要发给谁"。
理解Socket
- IP地址用来标识主机的唯一性,port端口号用来标识该台主机上 唯一的一个进程。
- 所以IP+port就能 表示互联网中唯一的一个进程。
- 所以,通信的时候,本质是两个互联网进程在通信,所以,网络通信的本质是进程间通信。
- 我们把IP+port叫做套接字(Socket)。
2,传输层的典型代表
传输层是属于内核的,所以进行网络通信,必定调用的是传输层提供的系统调用。
TCP协议:
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
UDP协议:
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
3,网络字节序
内存中多字节数据相对于内存地址有大端和小端之分。
磁盘文件中的 多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端
之分。
所谓小端,就是对于多字节的数据,它的低权值位存放在低地址处,高权值位存放在高地址处。
大端与之相反,低权值位存放在高地址处,高权值位存放在低地址处 。

发送主机通常将发送的数据按内存地址从低到高发出。
接受主机把从网络上接受的数据依次保存在缓冲区,也是按内存地址从低到高的顺序保存的。
而不同的主机,它的存储序列可能不同,可能是大端存储,也有可能是小端存储。
如果两台机器的存储形式不同,一台是大端存储,另一台是小端存储,那么在接受数据的时候,就会将数据解释错了。
解决方法:于是就有了一个规定, 发送到网络中的数据必须是大端形式的。
所以,不管这台主机 是大端序列还是小端序列,都会按照这个规定的网络字节序来发送/接受数据。
如果当前主机是小端,就需要先将数据转化为大端;如果是大端,就忽略。
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数来做主机字节序和网络字节序的转化:

- h表示host,n表示network,l表示32位长整数,s表示16位短整数。
- 例如htonl表示将32位的长整数从主机字节序转换为网络字节序。
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。
4,Socket编程接口
常见API
C
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器 )
int socket(int domain,int type,int protocol);
//绑定端口号(TCP/UDP,服务器)
int bind(int socket,const struct sockaddr* address,sock_lent addresslen);
//开始监听socket(TCP,服务器)
int listen(int socket,int backlog);
//接收请求(TCP,服务器)
int accept(int socket,struct sockaddr* address,sock_lent* addresslen);
//建立连接(TCP,客户端)
int connect(int sockfd,const struct sockaddr* addr,sock_lent* addrlen);
在上面的API接口中,sockaddr这个结构体经常出现。它是什么呢?
sockaddr结构

二,Echo Server(UDP实现)
Echo Server(回显服务器)是一种网络应用程序。其核心功能是接受客户端发来的数据,并将接受到的数据原样返回给客户端。
核心逻辑:
服务端
创建套接字 → 绑定地址 → 监听连接 → 接受请求 → 读取数据 → 回传数据。
客户端
创建套接字 → 连接服务器 → 发送数据 → 接收回显数据。
不过我们实现的是UDP的,所以没有监听连接这一步。
1,服务器端(server端)代码编写
这里对服务器端代码编写时,会采用面向对象的思路,简单的进行封装。
大致框架:
class udpserver
{
public:
udpserver(uint16_t port)
:_port(port),
_isrunning(false)
{}
//
/
~udpserver()
{}
private:
int _sockfd;//套接字
uint16_t _port;//端口号
bool _isrunning;//是否在进行网络通信
};
下面就是正式对网络通信的代码编写:
首先,定义一个init方法,在该函数中,完成创建套接字和绑定的任务。
创建套接字(socket)

关于该函数的返回值 ,文档中的说明如下:

可以看出,该函数的返回值是一个文件描述符,-1表示失败,和文件系统中的open一样。所以可以简单的理解成创建套接字,在系统内部会打开一个文件,然后分配一个文件描述符。
//1,创建套接字
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd<0)
{
std::cout<<"创建套接字失败"<<std::endl;
exit(1);
}
std::cout<<"创建套接字成功"<<std::endl;
绑定端IP和 端口号
为当前服务器进程绑定端口号和IP地址。
前面讲过IP+端口号可以标识网络中某台主机上的唯一一个进程。

在绑定之前,需要我们填写addr这个结构体中的内容。
我们是进行网络通信,所以需要填写下图中的sockaddr_in结构体,有三个成员。最后的8字节用0填充即可。

我们在填写该结构体中的端口号时,考虑到不同机器的大小端存储形式可能不一样。所以需要先将 端口号转化为网络字节序,使用htons接口。
同时IP地址在填充的时候 ,还有一个问题,具体内容看下面代码的注释:
//2,绑定端口号和IP
//填充sockarr_in信息
struct sockaddr_in local;
//先将结构体内容清0
bzero(&local,sizeof(local));
local.sin_family=AF_INET;//头部的16为,表示网络通信
local.sin_port=htons(_port);//端口号
//INADDR_ANY是一个宏,这个宏的值是0
//可能存在多个客户端要访问该服务器进程
//有的客户端拿着内网IP,有的客户端拿着本地回环IP 127.0.0.1访问该服务器进程
//那么该服务器进程的IP就必须保持不变,是一个绑死的值,只有和该IP相等的客户端才能访问
//将IP地址设为0的好处是:
//不同的客户端,拿着不同的IP访问该服务器进程,只要端口号和该进程相等,就都可以访问呢
local.sin_addr.s_addr=INADDR_ANY;//IP地址
int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
std::cout<<"绑定失败"<<std::endl;
exit(2);
}
std::cout<<"绑定成功"<<std::endl;
接受消息(recvfrom)
再定义一个start方法 ,来实现接受消息和发送消息。

- 第一个参数是创建的套接字
- 第二个参数和第三个参数是缓冲区及对应的大小,用来存放接受到的数据
- 第四个参数flags:该参数设置为0,表示阻塞式IO,如果对方 不发数据,该函数(进程)就会一致阻塞在这里等同于scanf
- 第五个参数:我们作为服务器端,需要知道是哪个主机的哪个进程发的数据,就存放在该结构体中。
- 最后一个参数:表示该结构体的大小
//缓冲区------存放消息
char buffer[1024];
//获取哪台主机的哪个进程发送的数据,可以理解为这是一个输出型参数
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//接受消息
//我们将接受到的消息是按字符串存储的
//这里sizeof(buffer)-1是为了保留最后一个位置填充1
ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
注意:我们从网络中拿到了发送过来的数据,这些数据包括struct sockaddr结构体。
所以我们想要知道是哪台主机的哪个进程发送过来的,只要拿到该结构体中的IP和端口号即可。但是这些字段是从网络中来的,都是大端形式的。所以我们还需再做一步,将网络字节序转化为主机字节序,可以使用ntohs接口。
发送消息
服务器端接收到客户端发来的消息后,要进行出来后,再返回给客户端。也就是用户做了某个要要求,服务器端执行完后,返回给用户一个结果。

- 第一个参数:创建的套接字
- 第二个参数和第三个参数:发送数据的内容和长度
- 第四个参数:和上面接受消息的一样
- 最后两个参数:发送数据,需要知道是给谁发的,也就是目标主机的IP和端口号,这些内容存放于该结构体中
对于接受到的数据,进行处理,再返回给客户端一个返回结果。所以我们可以自定义一个处理函数,对接受到的数据执行某种方法,再将结果返回给用户。
//获取发送方的信息
//获取端口号(客户端的)
int peer_port=ntohs(peer.sin_port);//网络字节序转主机字节序
//获取IP(点分十进制格式)
//该函数做两个工作 1,将网络字节序转化为主机字节序 2,将主机字节序的IP再转化为点分十进制形式
std::string peer_ip=inet_ntoa(peer.sin_addr);
buffer[n]=0;//接受到的数据
//处理buffer,自定义一个函数
//产生一个处理结果,返回给发送方,也就是客户端
std::string result=_func(buffer);
//发送消息
sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);
2,客户端(client端)代码编写
客户端代码编写与服务器端代码类似,比服务器端代码更简单。
客户端也需要创建套接字:
//以命令行参数的形式获取IP和端口号
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];//IP
uint16_t server_port = std::stoi(argv[2]);//端口号
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
return 0;
}
但是客户端不需要显示的绑定端口号和IP了 。当客户端尝试发送消息的时候,操作系统会为该客户端进程分配一个随机端口号,操作系统也知道本主机的IP地址,所以操作系统会在内部进行绑定。所以就不需要我们手动绑定了。
而为什么要这么做?
首先,一个端口号只能被一个进程绑定。
如果我们手动显示的绑定了,也就是我们把一个进程和一个端口号绑定在一起,绑死了。比如我们写了一个客户端,绑定一个端口号666,它没有运行。那么当再启动一个淘宝APP时,如果淘宝也绑定了这个端口号,那么我们的客户端进程就无法启动了。
所以,这样采用随机端口的方式,是为了避免client端口冲突的问题。
所以,客户端对应的端口号是多少不重要,只要是唯一的就行。
而为什么服务器端需要绑定?首先,服务器肯定是被多个客户端进行访问的。
服务器的IP和端口号必须是众所周知且不能轻易改变的。
改变了,客户端就无法访问了。
这就好像再生活中,一些公共部门的电话是众所周知,且不能改变的,比如110,120,119等等。而我们的电话是可以改的。
所以创建套接字后,就可以发送消息了,不需要显示bind了。
发送消息,接受消息和服务器端的代码类似,这里不做过多赘述了。
3,代码总体
udpserver.hpp文件
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <functional>
using func_t=std::function<std::string(const std::string&)>;
class udpserver
{
public:
udpserver(uint16_t port,func_t func)
:_port(port),
_isrunning(false),
_func(func)
{}
//
void init()
{
//1,创建套接字
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd<0)
{
std::cout<<"创建套接字失败"<<std::endl;
exit(1);
}
std::cout<<"创建套接字成功"<<std::endl;
//2,绑定端口号和IP
//填充sockarr_in信息
struct sockaddr_in local;
//先将结构体内容清0
bzero(&local,sizeof(local));
local.sin_family=AF_INET;//头部的16为,表示网络通信
local.sin_port=htons(_port);//端口号
//INADDR_ANY是一个宏,这个宏的值是0
//可能存在多个客户端要访问该服务器进程
//有的客户端拿着内网IP,有的客户端拿着本地回环IP 127.0.0.1访问该服务器进程
//那么该服务器进程的IP就必须保持不变,是一个绑死的值,只有和该IP相等的客户端才能访问
//将IP地址设为0的好处是:
//不同的客户端,拿着不同的IP访问该服务器进程,只要端口号和该进程相等,就都可以访问呢
local.sin_addr.s_addr=INADDR_ANY;//IP地址
int n=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
if(n<0)
{
std::cout<<"绑定失败"<<std::endl;
exit(2);
}
std::cout<<"绑定成功"<<std::endl;
}
void start()
{
_isrunning=true;
while(_isrunning)
{
//缓冲区------存放消息
char buffer[1024];
//获取哪台主机的哪个进程发送的数据,可以理解为这是一个输出型参数
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//接受消息
//我们将接受到的消息是按字符串存储的
//这里sizeof(buffer)-1是为了保留最后一个位置填充1
ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);
if(n>0)
{
//获取发送方的信息
//获取端口号(客户端的)
int peer_port=ntohs(peer.sin_port);//网络字节序转主机字节序
//获取IP(点分十进制格式)
//该函数做两个工作 1,将网络字节序转化为主机字节序 2,将主机字节序的IP再转化为点分十进制形式
std::string peer_ip=inet_ntoa(peer.sin_addr);
buffer[n]=0;//接受到的数据
//处理buffer,自定义一个函数
//产生一个处理结果,返回给发送方,也就是客户端
std::string result=_func(buffer);
//发送消息
sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);
}
}
}
/
~udpserver()
{}
private:
int _sockfd;//套接字
uint16_t _port;//端口号
bool _isrunning;//是否在进行网络通信
func_t _func;//回调函数
};
udpserver.cpp文件
#include "udpserver.hpp"
#include <memory>
std::string defaulthandler(const std::string &message)
{
std::string hello = "hello, ";
hello += message;
return hello;
}
//端口号以命令行参数的形式获取
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return 1;
}
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<udpserver> usvr = std::make_unique<udpserver>(port,defaulthandler);
usvr->init();
usvr->start();
return 0;
}
udpclient.cpp文件
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
//以命令行参数的形式获取IP和端口号
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
return 1;
}
std::string server_ip = argv[1];//IP
uint16_t server_port = std::stoi(argv[2]);//端口号
// 1. 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0)
{
std::cerr << "socket error" << std::endl;
return 2;
}
// 填写服务器信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while(true)
{
std::string input;
std::cout << "Please Enter# ";
std::getline(std::cin, input);
//发送消息
int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server));
(void)n;
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
//接受消息
int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
if(m > 0)
{
buffer[m] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
4,现象演示
