本文专栏:Linux网络编程
IP地址:
作用:
端口号:
所以IP地址+端口号能够标识网络上的某一台主机的某一个进程。

在操作系统中,我们知道pid表示唯一一个进程。而这里端口号也是表示唯一一个进程 。这两个之间有什么关系?
进程ID属于系统概念,技术上 也具有唯一性,确实可以用来标识唯一的一个进程 。但是如果让进程ID代替端口号,会让系统进程管理和网络强耦合 。
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号。
就是在 描述”数据是谁发的,要发给谁“。
传输层是属于内核的,所以进行网络通信,必定调用的是传输层提供的系统调用。
TCP协议:
UDP协议:
内存中多字节数据相对于内存地址有大端和小端之分。
磁盘文件中的 多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端
之分。
所谓小端,就是对于多字节的数据,它的低权值位存放在低地址处,高权值位存放在高地址处。 大端与之相反,低权值位存放在高地址处,高权值位存放在低地址处 。

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

常见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这个结构体经常出现。它是什么呢?

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

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

可以看出,该函数的返回值是一个文件描述符,-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+端口号可以标识网络中某台主机上的唯一一个进程。

在绑定之前,需要我们填写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;再定义一个start方法 ,来实现接受消息和发送消息。

//缓冲区——存放消息
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接口。
服务器端接收到客户端发来的消息后,要进行出来后,再返回给客户端。也就是用户做了某个要要求,服务器端执行完后,返回给用户一个结果。

对于接受到的数据,进行处理,再返回给客户端一个返回结果。所以我们可以自定义一个处理函数,对接受到的数据执行某种方法,再将结果返回给用户。
//获取发送方的信息
//获取端口号(客户端的)
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);客户端代码编写与服务器端代码类似,比服务器端代码更简单。
客户端也需要创建套接字:
//以命令行参数的形式获取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地址,所以操作系统会在内部进行绑定。所以就不需要我们手动绑定了。
而为什么要这么做?
而为什么服务器端需要绑定?
这就好像再生活中,一些公共部门的电话是众所周知,且不能改变的,比如110,120,119等等。而我们的电话是可以改的。
所以创建套接字后,就可以发送消息了,不需要显示bind了。
发送消息,接受消息和服务器端的代码类似,这里不做过多赘述了。
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;
}