目录
1.认识端口号
1.1 理解端口号和进程ID
1.2 理解源端口号和目的端口号
2.认识TCP协议
3.认识UDP协议
4.网络字节序
5.socket编程接口
5.1socket常见API
5.2sockaddr结构
sockaddr结构
sockaddr_in 结构
in_addr结构
6.简单的UDP网络程序
6.1创建套接字
6.2 绑定网络信息 指明IP+prot
6.3 udpServe代码
6.3.1 udpServe通用服务器
6.3.2 实现简单通信的服务器
6.4 udpClient代码
6.5本地测试
在上一讲中我们知道了网络传输的基本流程,本节我们要更加深刻的理解一下两台主机之间交互的本质。
我们在网络通信的时候,只要让两台主机能够通信就可以了吗??
实际上,在进行通信的时候不仅仅要考虑两台主机间相互交互数据!!本质上将,进行数据交互的时候是用户和用户在进行交互用户的身份,通常是用程序体现的!!程序一定是在运行中 --> 进程!!
因此主机间通信的本质是:在各自的主机上的两个进程在互相交互数据!!也就是进程间通信
IP地址可以完成主机和主机的通信,而主机上各自的通信进程才是发送和接受数据的一方。
因此我们用IP--确保主机的唯一性,再加端口号(prot)来确保该主机上的进程的唯一性.
IP:PORT = 标识互联网中唯一的一个进程!!
因此我们把IP:PORT叫做socket(套接字),因此网络通信的本质:也就是进程间通信!!
我们通常把本地的进程间通信称作SystemV进程间通信。
网络间的进程间通信用的是process,Socket.
1.认识端口号
我们刚说过端口号PROT是用来确保主机上进程的唯一性:他要告诉主机以后把消息交给哪一个进程。
端口号(port)是传输层协议的内容:
- 端口号是一个2字节16位的整数
- 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给哪一个进程来处理
- IP地址+端口号 能够标识网络上的某一台主机的某一个进程
- 一个端口号只能被一个进程占用
1.1 理解端口号和进程ID
我们之前在学习进程的时候,学习到过pid是用来标识进程的,那么此处的端口号也是唯一表示一个进程的。那么这两者又有什么关系呢?
端口号是个数字,标定进程唯一性,更加是一种证明,证明这个进程要进行网络通信,没有端口号,这个进程可能只是本地进程。因此端口号和进程ID的差别就是这样。
因此一个进程可以绑定多个端口号,但是一个端口号只能绑定一个进程!!
1.2 理解源端口号和目的端口号
源端口号和目的端口号是传输层协议(tcp和udp)的数据段中有两个端口号,就是在描述“数据是谁发的,要发给谁”。
2.认识TCP协议
此处我们先对TCP(Transmission Control Protocolc 传输控制协议)有一个直观的认识,后面我们再详细讨论TCP的一些细节
- 传输层协议
- 有连接
- 可靠传输
- 面向字节流
有连接:类似于打电话,两者要建立连接
可靠传输:仍然是打电话,我说的话你都听到了,你都收到了。
3.认识UDP协议
UDP(User Datagram Protocol 用户数据报协议)
- 传输层协议
- 无连接
- 不可靠传输
- 面向数据报
无连接:类似于发邮箱,不管你在不在,我都可以给你发邮件。
不可靠传输:仍然是发邮件,我发送给你,你看不看是你的事情。
4.网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?
- 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
- 接收主机把从网络上接到的字节一次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址
- TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节
- 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接受数据
- 如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略,直接发送即可。
主机序列转网络序列<-->网络序列转主机序列
记住一个不变的准则,网络序列一定是大端的,再根据你本地主机的字节序选择转或者不转。
为使网络程序具有可移植性,是同样的C代码在大端和小端计算机上编译后都能正常运行,可调用以库函数做网络字节序和主机字节序的转换
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint32_t netshort);
- 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数
- 例如 htonl 表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
- 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
- 如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回
5.socket编程接口
5.1socket常见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结构,那么这是一个什么结构呢?
5.2sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4,IPv6,以及UNIX Domain Socket(域间套接字 -- 不跨网络),然后各种网络协议的地址格式并不相同。
因此我们只要拿到sockaddr结构我们强转成sockaddr类型,读取前16位,如果是AF_INET则是跨网络通信,如果是AF_UNIX则是域间通信。因此我们这里都是网络通信,因此前16位都是AF_INET.
- 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位整数
6.简单的UDP网络程序
上面的准备工作结束,我们可以实现一个简单的英译汉的功能
6.1创建套接字
int socket(int domain,int type,int protocol);
返回值:如果成功,返回一个新的文件描述符。失败返回-1。
6.2 绑定网络信息 指明IP+prot
6.3 udpServe代码
6.3.1 udpServe通用服务器
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include <unistd.h>
#include "Log.hpp"
static void Usage(const std::string proc)
{
std::cout<<"Usage:\n\t"<<proc<<"port [ip]" <<std::endl;
}
//udp服务器
class UdpServer
{
public:
UdpServer(int port,std::string ip="")
:port_((uint16_t)port)
,ip_(ip)
,sockfd_(-1)
{}
~UdpServer(){}
public:
void init()
{
//1.创建socket套接字
sockfd_ = socket(AF_INET,SOCK_DGRAM,0);//就是打开了一个文件
// 无连接的 不可靠的
if(sockfd_ < 0)
{
logMessage(FATAL,"socket:%s:%d",strerror(errno),sockfd_);
exit(1);
}
logMessage(DEBUG,"socket create success : %d",sockfd_);
// 2.绑定网络信息 指明IP+prot
// 2.1 先填充基本信息到struct sockaddr_in
struct sockaddr_in local;
bzero(&local,sizeof(local));//memset
local.sin_family = AF_INET;//协议家族 前2个字节 16位 域的概念
//填充服务器的端口号信息,是会发给对方的port_一定会到网络中
local.sin_port = htons(port_);
// 服务器都必须具有IP地址,
//"aaa.zzz.yy.xx" 字符串风格的点分十进制->4字节IP->uint32_t ip
// INADDR_ANY(0) 程序员不关心会bind到哪一个ip,任意地址bind,强烈推荐的做法
// inet_addr:指定填充确定的ip,特殊用途,测试时使用;除了做转换,还会自动给我做序列转换
local.sin_addr.s_addr = ip_.empty()?htonl(INADDR_ANY):inet_addr(ip_.c_str());
// 2.2 bind网络信息
if(bind(sockfd_,(const struct sockaddr*)&local,sizeof(local)) == -1)
{
logMessage(FATAL,"bind:%s:%d",strerror(errno),sockfd_);
exit(2);
}
logMessage(DEBUG,"socket bind success : %d",sockfd_);
// done
}
void start()
{
//服务器设计的时候都是死循环
//将来读取到的数据都放在这里
char inbuffer[1024];
//将来发送的数据都放在这里
char outbuffer[1024];
while(true)
{
// struct sockaddr_in peer;//远端 输出型参数
// socklen_t len = sizeof(peer);//输入输出型参数
// //demo 2
// //后面的两个参数是输出型参数,谁给你发的消息 和客户端多长
// ssize_t s = recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,
// (struct sockaddr*)&peer,&len);
logMessage(NOTICE,"serve 提供 service中...");
sleep(1);
}
}
private:
//服务器端口号信息
uint16_t port_;
//服务器必须得有ip地址
std::string ip_;
int sockfd_;//套接字信息
};
// ./udpSever port [ip]
int main(int argc,char* argv[])
{
if(argc != 2 && argc != 3)
{
Usage(argv[0]);
exit(3);
}
uint16_t port = atoi(argv[1]);
std::string ip;
if(argc == 3)
{
ip = argv[2];
}
UdpServer svr(port,ip);
svr.init();
svr.start();
return 0;
}
我们在用指令也可以查看当前的服务信息
netstat -lnup
- 第一列为服务类型(Proto):当前为udp
- 第二列表示有没有收到消息(Recv-Q):0表示没有
- 第三列表示有没有发送消息(Send-Q):0表示没有
- 第四列表示本地绑定的ip地址:为全0表示任意地址绑定,绑定的端口号是8080
- Foreign Address :表示允许远端的任何主机任何端口给我发消息
注意:云服务器不能显示的写端口号,因为云服务器不允许你绑定云服务器公网IP
因此我们一旦实现绑定云服务器ip会报错。我们直接不写让他任意绑定即可
6.3.2 实现简单通信的服务器
我们完成服务器编写的基本框架后,我们想要实现一个可以将客户端传入的字符进行大小写转换的服务。具体服务如下:
void start()
{
//服务器设计的时候都是死循环
//将来读取到的数据都放在这里
char inbuffer[1024];
//将来发送的数据都放在这里
char outbuffer[1024];
while(true)
{
struct sockaddr_in peer;//远端 输出型参数
socklen_t len = sizeof(peer);//输入输出型参数
//demo 2
//后面的两个参数是输出型参数,谁给你发的消息 和客户端多长
ssize_t s = recvfrom(sockfd_,inbuffer,sizeof(inbuffer)-1,0,
(struct sockaddr*)&peer,&len);
if(s>0)
{
inbuffer[s] = 0;//当做字符串
}
else if(s == -1)
{
logMessage(WARINING,"recvfrom:%s:%d",strerror(errno),sockfd_);
continue;//服务器不能退出
}
//走到这里一定成功了 除了读取到对方的数据 还要读取对方的网络地址[ip:port]
std::string peerIp = inet_ntoa(peer.sin_addr);//拿到了对方的ip
uint32_t peerPort = ntohs(peer.sin_port);//拿到了对方的port
//打印出来客户端给服务器发送过来的消息
logMessage(NOTICE,"[%s:%d]]# %s",peerIp.c_str(),peerPort,inbuffer);
// logMessage(NOTICE,"serve 提供 service中...");
// sleep(1);
}
}
6.4 udpClient代码
我们服务器写完之后,现在来编写客户端的代码
客户端在编写的时候不需要bind,为什么?
所谓的"不需要"指的是:不需要用户自己bind端口信息 因为OS会自动给你绑定,如果我们非要自己bind 可以 但是不推荐!!!
所有的客户端软件<- ->服务器 通信的时候 必须得有client[ip:port] <-->serve[ip:port]。为什么?
因为client很多,不能给客户bind指定的port,因为port可能被别的client使用了. 你的client就无法启动了
那么Server凭什么要bind呢?
server提供的服务,必须被所有人都知道!而且server不能随便改变!
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <sys/socket.h>
#include <assert.h>
static void Usage(std::string name)
{
std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}
// udpClient server_ip server_port
// 如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(1);
}
// 1.根据命令行 设置要访问的服务器IP
std::string server_ip = argv[1];
uint16_t server_port = atoi(argv[2]);
// 2.创建客户端
// 2.1 创建socket
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
assert(sockfd > 0);
// 2.2 client 不需要bind
// 所谓的"不需要" 指的是:不需要用户自己bind端口信息 因为OS会自动给你绑定
// 如果我们非要自己bind 可以 但是不推荐
// 所有的客户端软件<- ->服务器 通信的时候 必须得有client[ip:port] <-->serve[ip:port]
// 为什么?client很多,不能给客户bind指定的port,因为port可能被别的client使用了
// 你的client就无法启动了
// 2.2 填写服务器信息
struct sockaddr_in server;
bzero(&server, sizeof server);
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
// 3.通信过程
std::string buffer;
while (true)
{
std::cout << "Please Enter# ";
std::getline(std::cin, buffer);
// 发送消息给sever
// 首次调用sendto函数的时候 我们的client
sendto(sockfd, buffer.c_str(), buffer.size(), 0,
(const struct sockaddr *)&server, sizeof(server));
}
return 0;
}
6.5本地测试
其中127.0.0.1 是本地换回的端口号,也就是本主机
当然如果大家有多台主机就可以跨网络传输通信啦!
这是我朋友链接我的主机给我发送的消息(跨主机网络通信)
注意:如果是云服务器的话,大家一定要手动开端口号,否则是不能使用指定端口号的
(本篇完)