网络预备
知识学习:https://blog.csdn.net/hansionz/article/details/85224786
网络编程套接字
一.IP地址和端口号
1.IP地址
IPv4
版本的IP地址为4字节
,也就是32位
- 网络层的数据报中封装
两个IP地址
,一个源IP地址(数据报源主机的IP)
,一个目的IP地址(数据报目的主机的IP)
源IP地址
和目的IP地址
相当于取经
的例子,在预备知识
中可以看到- 一个数据报的头部不应该只存在源IP地址和目的IP地址,还应该存在一个
协议字段
告诉应该交给上层的哪一个协议
2.端口号
2.1 什么是端口号
- 端口号是
传输层
的概念 - 端口号是一个
2字节16位
的整数 - 端口号用来
标识一个进程
,用来告诉OS
,将数据交给哪一个进程
IP地址+端口号
可以唯一的表示网络中一个主机的进程
一个
端口号只能被一个进程占用
2.2 端口号和进程ID
一个进程PID
也可以唯一的标识一个进程
,那为什么还需要使用端口号
来标识一个主机中的进程呢?
- 当一个
进程退出
时,在次启动进程
时,它的PID
已经变化。所以要将进程和端口号绑定
来标识这个进程 一个进程
可以绑定多个端口号
,但是一个端口号
只能被一个进程
绑定
2.3 源端口号和目的端口号
传输层
协议TCP/UDP
的数据段
中,也存在两个字段,一个是源端口号
,一个是目的端口号
,它用来表示这个数据是哪一个进程发的
,要发给哪一个进程
,也就是谁发的要发给谁
。
二.初识TCP/UDP协议和网络字节序
1.TCP(传输控制协议)
传输层
协议- 面向
连接
,两个主机只有建立连接
之后才可以通信
可靠
传输,建立连接之后,占用端到端的通信资源
- 面向
字节流
,发送的字节流数据之间没有明确的间隔
2.UDP(用户数据报协议)
传输层
协议- 面向
无连接
,尽最大努力交付
(例如:发短信,不管能不能收到,都可以发送) 不可靠
的传输- 面向
数据报
,发送的一个数据是一个整体
,它们之间有明确的间隔
3.网络字节序
内存中大于1个字节的数据相对于内存地址存在大小端之分,磁盘文件中多字节数据相对于偏移地址也存在大小端之分,网络中的数据流也存在大小端之分。
什么是大小端之分?
单独总结我的另一篇博客:https://blog.csdn.net/hansionz/article/details/80871921
网络字节流的大小端:
发送主机
通常将发送缓冲区
中的数据按内存地址从低到高
的顺序发出。接收主机把从网络上接到的字节依次保存在接收缓冲区
中,也是按内存地址从低到高
的顺序保存。- 网络数据流的地址规定,
先发出
的数据是低地址
,后发出
的数据是高地址
。 TCP/IP
协议规定,网络数据流应采用大端字节序
,即低地址高字节
。- 不管这台主机是
大端机
还是小端机
, 都会按照这个TCP/IP
规定的网络字节序来发送/接收
数据。如果当前发送主机是小端
, 就需要先将数据转成大端
, 否则就忽略, 直接发送。
为使网络程序
具有可移植性
,使同样的C代码在大端和小端
计算机上编译后都能正常运行,可以调用以下库函数对网络字节序和主机字节序
的转换:
#include <arpa/inet.h>
//h--host主机,n--net网络,l--long长整型32位,s--short短整型16位
uint32_t htonl(uint32_t hostlong);//主机-->网络(32位)
uint16_t htons(uint16_t hostshort);//主机-->网络(16位)
uint32_t ntohl(uint32_t netlong);//网络-->主机(32位)
uint16_t ntohs(uint16_t netshort);//网络-->主机(16位)
三.Socket编程
1.Udp socket常见接口
- socket(创建套接字)
#include <sys/types.h>
#include <sys/socket.h>
//创建socket文件描述符,相当于创建一个设备文件,来实现传输层之间的通信
//(客户端+服务器)
int socket(int domain, int type, int protocol);
参数:
domain:代表通信协议族
AF_INET---IPv4的协议
AF_INET6--IPv6的协议
type:代表创建什么类型的套接字
SOCK_STREAM:流式套接字(TCP)
SOCK_DGRAM:数据报套接字(UDP)
protocol:具体的协议
0代表默认协议
返回值:
返回该套接字的文件描述符(句柄)
- bind(绑定端口号)
#include <sys/types.h>
#include <sys/socket.h>
//用来绑定端口号(服务器)
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
sockfd:socket接口的返回值,既创建套接字的描述符
addr:类型为struct sockaddr,该结构体下边有详细说明,参数代表地址
addrlen:地址信息长度
sockaddr结构体:
socket API
是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6
、UNIX Domain Socket
。 但是各种网络协议的地址格式
并不相同:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14]; /*地址信息*/
}
sockaddr_in结构体:
虽然socket API
的类型是sockaddr
, 但是我们真正在基于IPv4
编程时, 使用的数据结构是sockaddr_in
。 这个结构里主要有三部分信息地址类型、端口号、IP地址
。
struct sockaddr_in {
short sin_family; // 2 bytes e.g. AF_INET, AF_INET6
unsigned short sin_port; // 2 bytes e.g. htons(3490)
struct in_addr sin_addr; // 4 bytes see struct in_addr, below
char sin_zero[8]; // 8 bytes zero this if you want to
};
//in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
struct in_addr {
unsigned long s_addr; // 4 bytes load with inet_pton()
};
可以将sockaddr
类似于void*
,将sockaddr_in
类似于int*
:
- 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
结构体指针做为参数
2.地址转化函数
在sockaddr_in
中的成员struct in_addr sin_addr
表示32位
的IP
地址。但是实际中,我们通常使用点分十进制的字符串
来标识IP地址。所以,我们应该使用一些地址转化函数来对IP地址进行字符串到in_addr
的相互
转化。
- 字符串转
in_addr
的函数
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
int inet_pton(int af, const char *src, void *dst);
in_addr
转字符串的函数
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
注:其中inet_pton
和inet_ntop
不仅可以转换IPv4
的in_addr
,还可以转换IPv6
的in6_addr
。
inet_ntoa函数:
inet_ntoa
函数返回了一个char*
, 这个函数自己在内部为我们申请了一块内存
来保存ip的结果。man手册上说,inet_ntoa
函数将返回结果放到了静态存储区
, 不需要我们手动释放
。但是如果放在静态区
的话,这个函数多次被调用就会覆盖掉静态区
的内容。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
using namespace std;
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0x0;
addr2.sin_addr.s_addr = 0xffffffff;
char* p1 = inet_ntoa(addr1.sin_addr);
char* p2 = inet_ntoa(addr2.sin_addr);
cout << p1 << endl << p2 << endl;
return 0;
}
下边的运行结果可以看出IP地址确实被覆盖
了。
就上述情况而言,如果是多个线程去调用inet_ntoa
函数,会不会出现问题?
- Man手册明确提出
inet_ntoa
不是线程安全
的函数 - 但是我自己在
centos7
上测试,并没有出现问题, 可能内部的实现加了互斥锁
- 多线程环境下,推荐使用
inet_ntop
, 这个函数由调用者
提供一个缓冲区
保存结果,可以规避线程安全
问题
测试代码:
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
using namespace std;
void* Routine1(void* arg)
{
struct sockaddr_in* ptr = (struct sockaddr_in*)arg;
while(1)
{
sleep(1);
char* p = inet_ntoa(ptr->sin_addr);
cout << "pthread 1:" << p << endl;
}
return NULL;
}
void* Routine2(void* arg)
{
struct sockaddr_in* ptr = (struct sockaddr_in*)arg;
while(1)
{
sleep(1);
char* p = inet_ntoa(ptr->sin_addr);
cout << "pthread 2:" << p << endl;
}
return NULL;
}
int main()
{
struct sockaddr_in addr1;
struct sockaddr_in addr2;
addr1.sin_addr.s_addr = 0x0;
addr2.sin_addr.s_addr = 0xffffffff;
pthread_t t1;
pthread_t t2;
pthread_create(&t1, NULL, Routine1, (void*)&addr1);
pthread_create(&t2, NULL, Routine2, (void*)&addr2);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
测试结果:
3. 常见Tcp sockect接口
- sockect(创建套接字)
参数及返回值说明:
- 该函数打开一个
网络通讯端口
,成功像open
一样返回一个文件描述符
,调用出错返回1 - 应用程序可以像读写文件一样用
read/write
在网络上收发数据 - IPv4协议,
domain
参数指定为AF_INET
TCP
协议,type参数指定为SOCK_STREAM
,表示面向字节流
的传输协议protocol
参数默认为0即可
- Bind(绑定)
参数及返回值说明:
- 服务器程序所监听的
网络地址
和端口号
通常是固定不变的,客户端程序得知服务器程序的IP地址和端口号后就可以向服务器发起连接。服务器需要调用bind绑定
一个固定的网络地址和端口号。而客户端
不需要。bind
的作用是将参数sockfd
和myaddr
绑定在一起,使sockfd
这个用于网络通讯的文件描述符监听addr
所描述的地址和端口号
。 struct sockaddr *
是一个通用指针类型,addr
参数实际上可以接受多种协议的sockaddr
结构体,而它们的长度各不相同,所以需要第三个参数addrlen
指定结构体的长度。bind
成功返回0,失败返回-1。
- listen(监听)
参数及返回值说明:
listen
声明sockfd
处于监听状态
, 并且最多允许有backlog
个客户端处于连接等待状态
, 如果接收到更多的连接请求就忽略,这里设置不会太大(一般是5)
。设置backlog是为了更加合理的利用资源,假设我们没有设置backlog,所有的连接都被占满,当来一个连接时,我们将它拒绝,而这时,有一个连接释放就会导致这个资源的不合理利用。但是太大也不行,太大会导致排在后边的连接可能要等待很长的时间。listen
成功返回0,失败返回-1。
- accept(服务器接收连接请求)
参数及返回值说明:
-
三次握手
完成后,服务器调用accept
接受连接。如果服务器调用accept
时还没有客户端的连接请求,就阻塞等待
直到有客户端连接上来 -
addr
是一个输出型参数,accept
返回时传出客户端的地址和端口号
。如果给addr 参数传NULL
,表示不关心
客户端的地址 -
addrlen
参数是一个传入传出参数(value-result argument)
, 传入的是调用者提供的,缓冲区addr
的长度以避免缓冲区溢出
问题, 传出的是客户端地址结构体的实际长度
(有可能没有占满调用者提供的缓冲区)。 -
accept函数
返回值是一个新
的套接字描述符。新老套接字描述符就像古代饭店拉客的情况,老的套接字描述符是为了建立连接(对应门外拉客的工作人员),而新的套接字描述符是为了后续和客户端通信(对应室内的服务人员)。 -
connect(客户端请求建立连接)
参数及返回值说明:
- 客户端需要调用
connect
连接服务器。connect
和bind
的参数形式一致, 区别在于bind
的参数是自己的地址
, 而connect
的参数是对方的地址
connect
成功返回0,出错返回-1