1、套接字
套接字:端口号拼接到IP地址构成了套接字。
套接字socket= {IP地址:端口号}
每一条TCP连接唯一的被通信两端的两个端点(即两个套接字所确定)
2、socket函数
创建一个套接字,调用socket函数
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// 返回值: 成功返回文件(套接字)描述符,失败返回-1
domain(域)确定通信的特性,包括地址格式
domain(域) | 描述 |
---|---|
AF_INET | IPv4因特网域 |
AF_INET6 | IPv6因特网域 |
AF_UNIX | UNIX域 |
AF_UPSPEC | 未指定 |
参数type确定套接字的类型,进一步确定通信特征
type(类型) | 描述 |
---|---|
SOCK_DGRAM | 固定长度的、无连接的、不可靠的报文传输 UDP |
SOCK_STREAM | 有序的、可靠的、双向的、面向连接的字节流 TCP |
SOCK_SEQPACKET | 固定长度的、有序的、可靠的、面向连接的报文传递 |
SOCK_RAW | IP协议的数据报接口 |
参数 protocol 通常是0 , 表示为给定的域和套接字类型选择默认协议。
虽然套接字描述符本质上是一个文件描述符,但不是所有参数为文件描述符的函数都可以接收套接字描述符。
3、shutdown函数
套接字通信是双向的。可以采用shutdown函数来禁止一个套接字的I/O
#include <sys/socket.h>
int shutdown(int sockfd, int how);
//返回值 成功返回0, 失败返回-1
how | 描述 |
---|---|
SHUT_RD | 关闭读端 |
SHUT_WR | 关闭写端 |
SHUT_RDWR | 关闭读写 |
close能够关闭一个套接字,为何还要使用shutdown呢?
1、只有最后一个活动引用被关闭时,close才释放网络端点,这意味着如果使用dup/dup2复制一个套接字,那么要直到关闭了最后一个引用它的文件描述符才会释放这个套接字。
至于什么时候需要调用 shutdown() 函数,下节我们会以文件传输为例进行讲解。
close()/closesocket()和shutdown()的区别
确切地说,close() / closesocket() 用来关闭套接字,将套接字描述符(或句柄)从内存清除,之后再也不能使用该套接字,与C语言中的 fclose() 类似。应用程序关闭套接字后,与该套接字相关的连接和缓存也失去了意义,TCP协议会自动触发关闭连接的操作。
shutdown() 用来关闭连接,而不是套接字,不管调用多少次 shutdown(),套接字依然存在,直到调用 close() / closesocket() 将套接字从内存清除。
调用 close()/closesocket() 关闭套接字时,或调用 shutdown() 关闭输出流时,都会向对方发送 FIN 包。FIN 包表示数据传输完毕,计算机收到 FIN 包就知道不会再有数据传送过来了。
默认情况下,close()/closesocket() 会立即向网络中发送FIN包,不管输出缓冲区中是否还有数据,而shutdown() 会等输出缓冲区中的数据传输完毕再发送FIN包。也就意味着,调用 close()/closesocket() 将丢失输出缓冲区中的数据,而调用 shutdown() 不会。
使用shutdown断开连接过程
4、字节序
与同一台计算机上的进程进行通信时,一般不用考虑字节序。字节序是一个处理器架构特性,用于指示像整数这样的大数据类型内部的字节是如何排序的
网络字节序是大端模式
如何查看自己的环境是大端还是小端模式?
使用 union来测试即可,根据union的特性,union的大小,由占用字节最大的成员变量所决定(需要考虑字节对齐问题)
#include<stdio.h>
union bit{
short n;
char byte[2];
}; //二字节对齐
int main(int argc,char *argv[])
{
union bit i;
i.n = 0x1234;
if (i.byte[0] == 0x34) // 低地址数据
printf("little\n");
else
printf("big\n");
return 0;
}
5、大小端转换
1、系统调用函数转换
#include <arpa/inet.h>
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位整数
h(host) 表示"主机字节序"
n(network) 表示"网络字节序"
2、自己实现转换
uint32_t htonl32(uint32_t number)
{
uint32_t a = 0;
a |= ((number >> 24) & 0xFF); //将最高字节移到最低地址
a |= ((number << 24) & 0xFF000000);//将最低字节移到最高地址
a |= ((number >> 8) & 0xFF00);// 中间两个字节调换位置
a |= ((number << 8) & 0xFF0000);
return a;
}
uint16_t htons16(uint16_t number)
{
uint16_t a = 0;
a |= ((number >> 8) & 0xFF);
a |= ((number << 8) & 0xFF00);
return a;
}
6、地址格式
一个地址标识一个特定通信域的套接字端点,地址格式与这个特定的通信域相关。为使不同格式地址能够传入套接字函数,地址会被强制转换成一个通用的地址结构sockaddr;
sockaddr 通用地址结构
typedef unsigned short sa_family_t
struct sockaddr {
sa_family_t sa_family; //地址族 AF_XXXX 2字节
char sa_data[14]; //可变长度地址 包含IP地址、port 14 字节
};
IPV4套接字地址结构 sockaddr_in
struct sockaddr_in {
sa_family_t sin_family; /* 地址族 */ //2字节
in_port sin_port; /* 目的端口号 */ //2字节
struct in_addr sin_addr; /* IP地址*/ // 4 字节
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[8];/*填充字节*/ //8 字节 需要清零
};
struct in_addr {
unsigned int s_addr;
};
sockaddr与sockaddr_in的区别
sockaddr 是通用的,在后面的函数传参时,将sockaddr_in 转换成sockaddr即可
二者长度一样,都是16字节
sockaddr_in 是internet环境下套接字的地址形式。所以在网络编程中我们会对sockaddr_in结构体进行操作,使用sockaddr_in来建立所需的信息,最后使用类型转化就可以了。一般先把sockaddr_in变量赋值后,强制类型转换后传入用sockaddr做参数的函数:sockaddr_in用于socket定义和赋值;sockaddr用于函数参数。
7、点分十进制IP转网络字节序IP
点分十进制,转32位IP
1、 inet_addr 函数
in_addr_t inet_addr(const char *cp); //cp 点分十进制地址
//返回网络字节序的32位地址
2、inet_aton函数
int inet_aton(const char *cp, struct in_addr *inp);
//成功返回1 ,失败返回0, 出错返回-1
参数 | 描述 |
---|---|
cp | 点分十进制地址 |
inp | 转换结果空间的首地址 存放转换结果的变量的地址 |
3、inet_pton 函数
int inet_pton(int af, const char *src, void *dst);
//成功返回1 ,失败返回0, 出错返回-1
参数 | 描述 |
---|---|
af | IP地址类型 AF_INET(IPV4) AF_INET6(IPV6) |
src | 点分十进制地址数据 |
dst | 存放转换结果的缓冲区 如果是IPV4 需要4字节 ; IPV6需要16字节 |
32位IP转点分十进制
1、inet_ntoa 函数
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
//返回 点分十进制地址字符串的地址
2、inet_ntop函数
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
//返回值,成功,返回地址字符串指针,失败返回 NULL
参数 | 描述 |
---|---|
af | IP地址类型 AF_INET(IPV4) AF_INET6(IPV6) |
src | 32位网络字节序的二进制IP地址 |
dst | 保存点分十进制字符串的缓冲区 |
size | dst缓冲区的大小,两个常量 IPV4 :INET_ADDRSRTRLEN IPV6:INET6_ADDRSTRLEN |
8、将套接字与地址关联bind(服务器)
将客户端的套接字关联上一个地址没有多少新意,可以让系统选一个默认的地址。
对于服务器,需要给一个接收客户端请求的服务器套接字关联上一个众所周知的地址。
所以bind是用在服务器端
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
//返回值: 成功返回0, 失败返回-1
参数 | 描述 |
---|---|
sockfd | 套接字描述符 |
addr | 填充了 地址族、IP地址、port的sockaddr_in结构体, 传参时需要转换为sockaddr |
addrlen | addr的长度 |
对于正在使用的地址有以下一些限制
1、在进程正在运行的计算机上,指定的地址必须有效,不能指定一个其他机器的地址
2、地址必须和创建套接字时的地址族所支持的格式相匹配
3、地址中的port必须不小于1024,除非进程具有权限(root)
公认端口:从0到1023
小于256的端口作为保留端口
注册端口:端口号从1024到49151 .
动态和/或私有端口:从49152到65535。理论上,不应为服务分配这些端口。实际上,机器通常从1024起分配动态端口。
4、一般只能将一个套接字端点绑定到一个给定的地址(sockaddr)上
例子
int sock_fd = socket(AF_INET, SOCK_STREAM, 0); // IPV4 tcp
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr);
addr.sin_family = AF_INET; //IPV4 需要与socket中的指定一致
addr.sin_addr = inet_addr("192.168.51.122");
addr.sin_port = 5050;//大于1024
bind(sock_fd, (struct sockaddr*)&addr, sizeof(addr));
9、连接请求connect(客户端)
如果要处理一个面向连接的网络服务(TCP), 那么在开始交换数据之前,需要在请求服务的进程套接字(客户端)和提供服务的套接字(服务器)之间建立一个连接 (三次握手)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
// 返回值:成功返回0, 失败返回-1
参数 | 描述 |
---|---|
sockfd | 套接字描述符 |
addr | 填充了 地址族、服务器IP地址、服务器port的sockaddr_in结构体, 传参时需要转换为sockaddr |
addrlen | addr的长度 |
在connect中指定的sockaddr的地址是我们想与之通信的服务器地址。 connect会给调用者绑定一个默认的地址(本机IP),这也是客户端为什么不需要使用bind的原因 (TCP)
当尝试连接服务器时,可能会连接失败,这些错误可能是由一些瞬时条件引起的。
下面这个是处理瞬时connect错误的方法,如果一个服务器运行在一个负载很重的系统上,就很有可能发生这种错误
指数补偿算法,如果调用connect失败,进程会休眠一小段时间,然后进入下次循环尝试,每次循环休眠时间指数级增加,直到最大延时为2分钟左右
#define MAXSLEEP 128
int connect_try(int sockfd, const strcut sockaddr *addr, socklen_t addrlen)
{
int num_sec;
for(num_sec = 1; numsec <= MAXSLEEP; numsec <<= 1)
{
if (connect(sockfd, addr, addrlen) == 0)
// 成功连接
return 0;
if (num_sec <= MAXSLEEP/2) //休眠,然后再次重连
sleep(num_sec);
}
return -1; //超时, 返回 -1
}
10、 监听连接请求listen(服务器)
服务器调用listen 函数来宣告它愿意接收连接请求
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
//成功返回0 , 失败返回-1
参数 | 描述 |
---|---|
sockfd | socket函数返回的套接字 |
backlog | accept应答之前,允许在进入队列中等待的连接数目,最大值128 |
11、处理客户端的连接请求accept (服务器)
建立套接字连接,处理单个连接请求
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//成功返回 与客户端连接的套接字, 失败返回-1
参数 | 描述 |
---|---|
sockfd | 正在监听端口的套接字描述符 |
addr | 客户端的sockaddr的信息 |
addrlen | sockaddr长度 |
accept所返回的文件描述符是套接字描述符,该描述符连接到调用connect的客户端。
addr 存放的是调用connect与之连接的客户端的信息,地址族、IP地址、port, 如果服务器不关心,可以设置为NULL
如果服务器调用accept,并且当前没有连接请求,服务器会阻塞直到一个新的请求到来。
另外,服务器可以使用poll或select来等待一个请求的到来,在这种情况下,一个带有等待连接请求的套接字会以可读的方式存在
基于TCP网络编程模型
上图的编程模型 三次握手、与四次挥手
TCP多进程/多线程服务器模型
TCP多进程服务器模型
优点:安全,进程隔离,一个进程崩溃,不会导致服务器崩溃
缺点:占用资源较大
TCP多线程服务器模型
优点:占用资源较少
缺点:不安全,线程使用进程的资源,一个线程崩溃会导致整个进程崩溃