端口 && IP
在学习套接字编程之前,我们必须了解一下前缀知识。首先是IP和端口的作用。
在这之前,我们要明白一件事。那就是把数据从一台主机发送到另一台主机,是目的吗???当然不是!!我们要把数据从一台主机发送到另一台主机指定中的一个进程,再由这个进程对数据进行处理后返回给用户。这才是目的!!那么我们通过IP找到了指定的主机,如何找到主机上指定的进程呢?? 那么我们就需要用到端口号(port)。
IP : 标识全网唯一一台主机(不太准确,暂时这么理解)
port 端口号 : 标识主机中唯一的一个进程
所以,IP + port 是不是就可以确定全网唯一 一个进程? 因为 IP确定全网唯一一台主机,端口号确定主机上唯一的一个进程。所以 IP + port 就可以确定全网唯一 一个进程。
而这个时候,我们主机与主机之间的通信,就变成了进程与进程之间的通信!进程间通信的本质是什么?是让多个进程可以看到同一份资源!在本地主机中这份资源可以是内存,也可以是文件。但是在多台主机中,这份资源就是我们熟悉的网络!!
所以我们可以用server把数据发送到网络中,再由对端的client从网络中读。就类似于读文件和写文件的操作一样,就可以实现跨网络的进程间通信!!
注意!
端口一旦确定,那么就不能改变!!因为在对应的客户端必须要指定服务器的端口,而此时如果服务器改变了自己的端口号,那么客户端也必须做到相应的改变!但显然客户并不会意识到这一点,所以端口号一旦确定,不要轻易改变!!!
TCP && UDP
接下来我们再浅浅了解一下TCP和UDP 。
TCP协议的特点
- 传输层协议
- 会建立连接
- 可靠传输
- 面向字节流
UDP协议的特点
- 传输层协议
- 不建立连接
- 不可靠传输
- 面向数据报
有连接 && 无连接
显然,UDP和TCP都是传输层协议,那么建立连接这个怎么理解呢?这就很类似两个人打电话,必须要先有一方给另一方播电话。而另一方看到电话来了只有在接通之后两人才能进行通信。而这个过程就类似于TCP的连接过程。
而UDP是无连接的,就意味着你给对方发送数据压根就不用等对方确认。就相当于你给对方打电话,对方不能自己接通,而是电话自动接通。这就是无连接 。
可靠传输 && 不可靠传输
首先我们要知道,可靠和不可靠在这里都是一个中性词。并不是一个贬义词,因此不能因为UDP是不可靠传输就说UDP差。我们要知道,保证可靠性是有代价的!! 因为TCP提供了各种可靠性机制,所以这也就意味着TCP会非常复杂!而UDP是不可靠性传输,就意味着它比较简单。各有各的优势,还是看场景进行选择。
注意!!无论是UDP和TCP,没有明显的谁慢谁快。因为跨网络传输看的是两台主机之间的距离和当前的网络状态!!与协议关系并不大。
面向字节流 && 面向数据报
面向字节流就是以字节流的形式进行报文的发送,那么这就意味着发送的时候,报文可能并不完整!比如你主机的传输层缓冲区已经满了,但是你的报文只接收了前面一点点。就只能等缓冲区被提取后才会接收后面的数据。这种现象就是面向字节流的!
面向数据报则是每次发送/接收,都必须是完整的报文!不存在断截之后再续上的现象,这就叫做面向数据报传输!!
网络字节序
我们都知道内存的字节存储方式有大端和小端。
大端存储 高位放在低字节序,低位放在高字节序 如:0X11223344 在内存中存储是 11 22 33 44
小端存储 低位放在低字节序,高位放在高字节序 如:0X11223344 在内存中存储是 44 33 22 11
既然不同的机器有不同的存储方式,那你怎么保证你小端发送的数据和大端接收的数据是匹配的?
很简单!!只要规定在网络上传输的数据都必须按大端字节序来存储!!发送发如果是小端则只需要转到大端发送即可,接收方是小端再由大端转回小端即可,如果是大端则不需要操作。
但是如果让我们自己来转换,那是不是太麻烦了呢?所以操作系统提供了下面的接口来供我们转换序列。
c
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); //主机转网络序列,转成long类型
uint16_t htons(uint16_t hostshort); //主机转网络序列,转成short类型
uint32_t ntohl(uint32_t netlong); //网络序列转主机序列,转成long类型
uint16_t ntohs(uint16_t netshort); //网络序列转主机序列,转成short类型
其中的h代表主机host,to就是到的意思,n则是network网络的意思。l对应long,s对应short。所以上面的函数其实很好记忆。
Socket套接字编程
Socket是一个套接字,它的本质就是一个文件描述符。对应的是网卡文件,我们只需要往文件描述符里面写入,就可以把数据发送到网络。
函数介绍
套接字的创建(UDP/TCP/服务端/客户端)
c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
int domain : 通信范围,例如AF_UNIX,AF_LOCAL是本地通信,AF_INET是IPV4,AF_INET6是IPV6 int type: 通信类型,最常见的是TCP的SOCK_STREAM面向字节流,UDP的SOCK_DGRAM面向数据报 int protocol: 确定socket支持的哪个协议,一般默认为0即可。 返回值:一个文件描述符,小于0则代表创建套接字(打开文件)失败
端口号绑定(UDP/TCP/服务端/客户端)
对套接字绑定端口,一般服务器显示绑定。客户端OS自动绑定。
c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
int sockfd: 要绑定的套接字文件描述符 const struct sockaddr *addr :一个结构体的指针,该结构体包含了domain,port,ip等信息.... socklen_t addrlen:结构体体长度 返回值:成功返回0,绑定失败返回-1,或者返回错误码
这个结构体的首元素是一个16位地址类型,虽然下面的内容都不一样,但只要统一强转成 struct sockaddr,那么就可以拿到前16位。再根据前16位判断出结构体的类型,再做出对应的处理。我们可以把sockaddr 看成是一个父类,sockaddr_in 和 sockaddr_un 是它的子类。而无论我们是使用sockaddr_in还是sockaddr_un,我们都可以用sockaddr来接收。再根据前16位判断地址类型,做出选择。这就很像我们的多态,因为这个功能出来的时候,C语言没有void*指针这个功能,所以我们传入时必须要进行类型强转。
监听套接字(TCP/服务端)
UDP不会用到监听套接字这个函数,因为UDP是无连接的。而TCP是有连接的,所以必须把这个套接字设置为监听状态。
c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
int sockfd:要监听的套接字 int backlog:连接同时存在的最大数量(因为监听是有消耗的,如果大量连接过来,监听不过来了,那么就会把这些连接暂时存储起来,而backlog就是存储的最大数量。) 返回值: 成功返回0,监听失败返回-1,或者返回错误码
接收连接请求(TCP/服务端)
c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int sockfd 监听后的套接字
struct sockaddr *addr: 存储对端domain,port,ip的结构体 socklen_t *addrlen: 结构体的大小 返回值: 一个可以直接通信的套接字
建立连接(TCP/客户端)
c
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
int sockfd:的套接字
const struct sockaddr *addr: 对端的addr结构体,可以提取到对端的IP和端口
socklen_t addrlen: 要接收的长度。
UDP通信测试
介绍2个函数,UDP传输要用的两个函数。 因为udp是无连接的,所以需要特定的函数来进行通信。
sendto函数
c
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
该函数的功能是往网络套接字中发送数据,如果调用该函数时套接字还没有绑定端口,那么该函数会自动为套接字随机分配端口。
int sockfd: 套接字
void *buf, size_t len: 发送的数据
size_t len:发送数据的大小
int flags:发送方式,0为阻塞发送
const struct sockaddr *dest_addr:该参数包含着目标主机的IP和端口,根据该参数找到服务器
socklen_t addrlen:传入的sockaddr的长度
recvfrom函数
cpp
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
该函数的功能是往从网络套接字中读取数据。
int sockfd: 套接字
void *buf, size_t len 读取数据的缓冲区
size_t len:读取数据的大小
int flags:是否阻塞读取,0为阻塞
const struct sockaddr *dest_addr:输出型参数,该参数对端主机的IP和端口,可以用来获取对端的主机IP和端口
socklen_t addrlen:接收的sockaddr 的长度
udp代码演示
服务器代码
server.cc文件
cpp
#include "server.hpp"
#include <memory>
int main(int argc , char* argv[])
{
if(argc != 2) //命令行参数不为2就退出
{
std::cout << "Usage : " << argv[0] << " bindport" << std::endl; //打印使用手册
exit(1);
}
uint16_t port = atoi(argv[1]); //命令行传的端口转换成16位整形
std::unique_ptr<UdpServer> s(new UdpServer(port)); //创建UDP服务器
s->init(); //初始化服务器,创建 + 绑定
s->start(); //运行服务器
}
server.hpp 代码:
cpp
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
class UdpServer
{
private:
int _sock;
uint16_t _port;
public:
UdpServer(uint16_t port): _port(port) { }
~UdpServer() { close(_sock); }
void init()
{
_sock = socket(AF_INET,SOCK_DGRAM,0); //创建套接字
if(_sock < 0)
{
//创建失败
std::cout << "create socket failed...." << std::endl;
abort();
}
//绑定
struct sockaddr_in ser;
ser.sin_port = htons(_port); //填入端口
ser.sin_family = AF_INET; // 填入地址类型
ser.sin_addr.s_addr = INADDR_ANY; //填入IP地址
if(bind(_sock,(sockaddr*)&ser,sizeof ser) != 0) //绑定
{
//绑定失败
std::cout << "bind socket failed...." << std::endl;
abort();
}
}
void start()
{
struct sockaddr_in peer; //对端
socklen_t peer_len = sizeof peer;
char buff[1024] = {0};
while(1)
{
//接收对端发来的消息
int n = recvfrom(_sock,buff,1023,0,(struct sockaddr*)&peer,&peer_len);
buff[n] = 0;
if(read == 0) //对端关闭了
{
std::cout << "one client quit..." << std::endl;
continue;
}else if(read < 0) //读取出错了
{
std::cout << "read error..." << std::endl;
break;
}
//读取正常...
std::string clientip = inet_ntoa(peer.sin_addr); //获取对端的IP
uint16_t clientport = ntohs(peer.sin_port);// 获取对端的端口
//打印信息....
std::cout << "["<< clientip <<"] "<< clientport << ": say : " << buff << std::endl;
}
}
};
客户端代码
cpp
#include "client.hpp"
#include <memory>
int main(int argc , char* argv[])
{
if(argc != 3) //命令行参数少于3个退出,因为必须 ./client 服务器ip 端口号 才可以调用
{
std::cout << "Usage : " << argv[0] << " serverip serverport" << std::endl;
exit(1);
}
uint16_t port = atoi(argv[2]); //获取服务器端口
std::string ip = argv[1]; //获取服务器IP
std::unique_ptr<UdpClient> cli(new UdpClient(port,ip)); //创建客户端
cli->init(); //初始化客户端
cli->start(); //客户端运行
}
client.hpp
cpp
#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <arpa/inet.h>
class UdpClient
{
public:
UdpClient(uint16_t port , const std::string& ip) : _port(port), _svr_ip(ip){}
~UdpClient(){ close(_sock); }
void init()
{
//创建套接字
_sock = socket(AF_INET,SOCK_DGRAM,0);
if(_sock < 0)
{
std::cout << "create socket failed...." << std::endl;
abort();
}
}
void start()
{
struct sockaddr_in svr; //该结构体填入服务器的端口和IP
svr.sin_port = htons(_port); //填入端口
svr.sin_addr.s_addr = inet_addr(_svr_ip.c_str()); //填入IP
svr.sin_family = AF_INET; //设置地址类型,ipv4
int i = 1; //打印数字,随意,可要可不要
//char buff[1024] = {0}; 如果需要读数据可用的输出缓冲区
while(1)
{
std::string message = "hello server " + std::to_string(i++); //构建发送消息
//发送消息
sendto(_sock,message.c_str(),message.size(),0,(struct sockaddr*)&svr,sizeof svr);
//可以通过下面代码再接收服务器发送的数据,如果服务器有发送的话
// int n = recvfrom(_sock,buff,1023,0,nullptr,nullptr);
// buff[n] = 0 ;
// std::cout << "server say# " << buff << std::endl;
sleep(1); // 睡眠1s
}
}
private:
int _sock; //套接字
uint16_t _port; //服务器的端口
std::string _svr_ip; //服务的IP
};