目录
🌈前言
这篇文章给大家带来套接字的学习!!!
🌸1、背景知识
🍡1.1、理解源IP地址和目的IP地址
源IP地址和目的IP地址
-
在IP数据包头部中,有两个IP地址,分别叫做:源IP地址和目的IP地址
-
源IP地址就是标定广域网(公网)中主机唯一性的地址(发送数据方的IP地址)
-
目的IP地址就是“接收数据方”的IP地址(同样在广域网中具有唯一性)
我们只要有IP地址就能完成通信了吗?
-
不是的,想象一下发qq消息的例子,有了IP地址能够把消息发送到对方的机器上
-
但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析
-
IP地址只能帮我们找到对方主机,我们还要考虑与对方相互交互数据
-
比如:西游记中唐太宗要唐僧去西天拜见如来佛祖取得真经,唐僧不是去到西天就行了,他还要拜见如来佛祖取得真经,并且交付给唐太宗,光去到西天是没有用的!!!
🍢1.2、端口号
端口号(port)是传输层协议的内容
-
端口号是一个2个字节16位的整数
-
端口号用来标识一个主机上某个进程的唯一性
-
IP地址 + 端口号能够标识网络上的某一台主机的某一个唯一的进程(互联网中唯一的进程)
-
一个端口号只能被一个进程占用(唯一性,一般指服务器),但是多个进程可以服务相同的端口号
-
网络通信的本质其实也是进程间通信,都是二个进程在交互数据。本质上数据交互是用户与用户进行交互,用户的身份,通常是由程序(进程)体现的!
端口号和进程PID的关系
-
端口号是标定网络上唯一的进程,进程PID是标定主机上唯一的进程
-
实现时为了不将这二个概念强关联(强耦合),所以单独维护了一个端口号标定网络上的唯一进程,因为不是每个进程都是要进网络的!
源端口号和目的端口号
-
传输层协议(TCP和UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号
-
源端口描述的是:数据是谁发的,目的端口描述的是:要将数据发给谁!
🍧1.3、认识UDP协议
UDP(User Datagram Protocol 用户数据报协议)
特点:
-
传输层协议
-
无连接
-
不可靠传输
-
面向数据报
🍨1.4、认识TCP协议
TCP(Transmission Control Protocol 传输控制协议)
特点:
-
传输层协议
-
有连接
-
可靠传输
-
面向字节流
🍰1.5、网络字节序
前言
- 大端:数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中(不变)
- 小端:数据的低位保存在内存的高地址中,而数据的高位, 保存在内存的低地址中(反着存)
-
内存中的多字节数据相对于内存地址有大端和小端之分
-
磁盘文件中的多字节数据相对于文件中的地址也有大端小端之分
-
网络数据流同样有大端小端之分
如何定义网络数据流的地址呢?
-
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出
-
接收主机把从网络上接收的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存
-
网络数据流的地址是这样规定的:先发出的数据是低地址,后发出的数据是高地址
-
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节
-
不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据
-
总之,如果当前发送主机是小端,就需要先将数据转成大端;否则就忽略,直接发送即可,发送到网络后,数据传输到对方时,还要将网络字节序转换成对方主机的存储模式
网络字节序和主机字节序的转换
#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(uint16_t netshort);
//-------------------------------------
函数解析
-
h表示host(主机),n表示network(网络),l表示32位长整数,s表示16位短整数
-
htonl是主机转网络字节序,参数为长整数,htons参数则是一个短整型
-
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送
-
ntohl是网络转主机字节序,参数为长整型,ntohs参数是一个短整型
-
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回
-
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回
🌺2、套接字相关API
🍡2.1、socket常见API
下面是套接字的常见API,后面会详细解析
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 创建 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);
🍢2.2、struct sockaddr 结构体
上面几个API中都出现了struct sockaddr结构体指针,它是什么呢?有什么用?
-
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,比如:IPv4、IPv6,以及UNIX Domain Socket(本地进程间通信)
-
不同的网络协议,它们之间的地址类型是不同的,IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址
-
IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6
-
我们只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容,最后确定是什么网络协议
-
总之,sockaddr就是一个通用的类型,其他不同的类型的可以强转成它,然后通过前十六位地址来判断是什么网络通信协议,最后再强转回对应的结构体
-
比如:我们要用IPv4进行通信,定义一个sockaddr_in,填充里面的信息,传参时,只要强转会sockaddr结构体就行了,底层会判断它是什么协议,然后重新强转回去
sockaddr 结构
sockaddr_in 结构体
-
虽然socket api的接口是sockaddr,但是我们真正在基于IPv4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息:地址类型, 端口号, IP地址
-
sa_family:它就是我们要填充的地址类型(协议家族)
-
sin_port:它就是要我们填充的端口号
-
sin_addr:它就是需要我们填充的IP地址
in_addr 结构体
- in_addr:用来表示一个IPv4的IP地址。其实就是一个32位的整数,直接赋值即可
🍀3、UDP网络编程
🍡3.1、API
使用UDP网络通信所用到的接口比TCP少,因为它是不用进行连接的
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 创建 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);
// 读取网络数据到指定的缓冲区当中 -- 并且将对方的struct sockaddr信息填充到src_addr当中
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
// 将缓冲区的内容通过网络发送到对方主机(根据端口号和IP地址发送)
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// 指定填充确定IP地址, 转化字符串风格IP("xx.zz.yy.sss"),并且自动进行主机字节序转换网络字节序
in_addr_t inet_addr(const char *cp));
int socket(int domain, int type, int protocol);
-
作用:它是用来创建套接字文件的,创建成功返回一个文件描述符,失败返回-1,并且设置errno
-
domain:它需要我们填充协议家族(地址类型)来指定网络协议
-
type:它需要我们填充通信类型,UDP协议是面向数据报的,我们填充SOCK_DGRAM即可,如果是TCP协议,那么填充SOCK_STREAM(其他详细查看man手册)
-
protocol:套接口所用的协议,一般为0,不指定
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
-
作用:绑定网络信息(地址类型、端口号和IP地址)到内核中,成功返回0,错误返回-1,并且设置errno
-
address:sockaddr_in或sockaddr_un的地址,并且需要强转为sockaddr*
-
len:sockaddr_in或sockaddr_un结构体所占内存空间的大小
🍢3.2、简单的UDP网络程序
实现小写英文转大写服务器 – 使用UDP协议
#include <iostream>
#include <cerrno>
#include <cassert>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;
class UdpServer
{
public:
UdpServer(uint16_t port, string ip = "")
: port_(port), ip_(ip), sockfd_(-1)
{
assert