Unix 网络编程基础

本专题内容均来自 Stevens 先生的 Unix 网络编程 卷I

TCP 建立与中止

三次握手

在这里插入图片描述
从图中可以看出:

  • 服务端必须准备好接受外来的连接,称之为被动打开。调用的函数为 socket, bind 和 listen 。
  • 客户端主动的像服务端发起连接时,会向服务端发送一个 SYN 分节,它告诉服务端接下来数据的初始序列号,此分节通常不携带数据。调用的函数为 connect 。
  • 服务端收到连接请求后,需要确认客户端的 SYN, 也就是回发一个 ACK, 同时也要发送一个 SYN 分节,其包含了同一连接中发送数据的初始序列号。
  • 客户端随后确认来自服务端的 SYN。

Note:
关于初始序列号,客户端或者服务端在发送 SYN 是都会携带初始序列号,在确认 SYN 的 ACK 中,携带的序列号必须是这个初始序列号加1。如图中所示的 J 和 J+1; K 与 K+1。

tcp中 SYN 的常用选项
(1) MSS 选项(maximum segment size)
(2) 窗口规模选项
(3) 时间戳选项

那么问题来了:为什么采用三次握手而不是两次?
(1)为了防止已失效的连接请求发送到服务端,浪费资源。这种预防是很有必要的,因为这样可以防止很多恶意的网络攻击。
(2)为了同步序列号,实现可靠传输。tcp 是全双工的,如果只是两次握手, 至多只有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认。假定通过超时重传策略实现两次握手后双方都确认起时序列号,那么握手时间将增加,连接建立时间将变长。

四次挥手

在这里插入图片描述
以客户端主动关闭为例

  • 客户端发送一个 FIN 分节,告知服务端,客户端数据发送完毕,即将关闭。调用 close 函数。
  • 服务端接收到来自客户端的 FIN 后,即可回复确认 ACK, 并将 FIN 放在队列中等候处理。
  • 服务端在处理完这个连接的所有数据后,也会调用 close 关闭,即向客户端发送 FIN 分节。
  • 客户端收到 FIN 分节后,同样回复确认 ACK。此时客户端进入 TIME_WAIT 状态。

Note:
(1) Q: 什么情况下存在 TIME_WAIT 状态?
A: 主动发起关闭连接的一端会存在 TIME_WAIT 状态。
(2) Q: TIME_WAIT 状态存在的意义?
A: 主要有两个理由:
I) 可靠的实现TCP 全双工连接的终止。因为最后的确认 ACK 可能会丢失,如果没有 TIME_WAIT 状态,被动关闭的一方将会超时重传 FIN, 此时主动关闭方已经关闭了,无法回传确认 ACK 了。
II) 允许老的重复分节在网络中消逝。TIME_WAIT 状态持续的时间为 2*MSL, 保证了相同 IP 和端口重复连接时,旧的重复分组已经在网络中消逝。

状态转换图

在这里插入图片描述

套接字编程

仅以 IPv4 套接字为例

在头文件 <netinet/in.h>中,POSIX 定义了 IPv4 的套接字地址结构:

struct in_addr{
	in_addr_t    s_addr;
}

struct sockaddr_in{
	uint8_t           sin_len;
	sa_family_t       sin_family;
	in_port_t         sin_port;
	
	struct in_addr    sin_addr;
	char              sin_zero[8];
}

通用套接字地址结构

struct sockaddr{
	uint8_t      sa_len;
	sa_family_t  sa_family;     //AF_xxx
	char         sa_data[14];
}

绑定套接字函数的 ANSI C 原型为

int bind(int, struct sockaddr*, socklen_t);

意味着在绑定套接字时,需要进行强制类型转换

struct sockaddr_in serv;
bind(sockfd, (struct sockaddr*)&serv, sizeof(serv));

需要注意:套接字数据结构大小的数据类型是 socklen_t,而不是 int。POSIX 建议将 socklen_t 定义为 uint32_t。

字节序

字节序没有标准,不同的系统可能是大端排序,也可能是小端排序的。

什么是大端和小端

在这里插入图片描述

如何判断系统的字节序

/**
* @return 0: unknown; 1: big-endian; 2: little-endian
*/
unsigned int host_byte_order()
{
    union 
    {
        short s;
        char c[sizeof(s)];
    }un;
    
    un.s = 0x0102;
    if(2 == sizeof(short))
    {
        if(1 == un.c[0] && 2 == un.c[1])
        {
            return 1;
        }
        else if(2 == un.c[0] && 1 == un.c[1])
        {
            return 2;
        }
    }

    return 0;
}

int main()
{
    printf("%s: ",CPU_VENDOR_OS);
    unsigned int ret = host_byte_order();
    if(1 == ret)
    {
        printf("big-endian\n");
    }
    else if(2 == ret)
    {
        printf("little-endian\n");
    }
    else
    {
        printf("unknown\n");
    }
    return 0;
}

在这里插入图片描述
关于 CPU_VENDOR_OS 宏:
随书源码根目录下运行 configure 文件即可生成 config.h文件,config.h文件中会有这个宏定义。
在这里插入图片描述

字节序间转换

#include <netinet/in.h>
//返回网络字节序
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);

//返回主机字节序
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net32bitvalue);

h: host
n: net
l: long
s: short

在网络编程时,统一转成网络字节序。例如端口13的转化

#include "byte_oder.h"
#include <netinet/in.h>

int main()
{
    test_host_byte_order();
    uint16_t port_n = htons(13);
    uint16_t port_h = ntohs(port_n);
    
    printf("port_n = %d, port_h = %d\n", port_n,port_h);

    return 0;
}

13 的16进制为 0x0D, 大端排序则为 0x0D00,转换10进制为 3328.
在这里插入图片描述

字节操作

#include <string.h>
//将目标字符串指定数目的字节清0
void bzero(void *dest, size_t nbytes);
//从源地址拷贝指定长度的字节到目的地址,与memcpy函数参数位置相反,并且memcpy函数在源字节串与目的字节串有重叠时可能出现不可预料的错误,而bcopy能正确处理。
void bcopy(const void *src,void *dest, size_t nbytes);
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);

注意:源字节串与目的字节串有重叠时用 memcpy 不安全的,可以使用 bcopy,也可以用 memmove函数代替。

地址转换函数

#include <arpa/inet.h>
/**
* @description 将 strptr 所指的字符串转换成32位网络字节序二进制值,并通过 addrptr指针存储
* @return 成功:1;失败:0 
*/
int inet_aton(const char* strptr, struct in_addr *addrptr);

/**
* 这是一个被废弃的函数
* 
* @description 与 inet_aton 函数功能一样,但是不能像 inet_aton 函数一样对 IP 执行检查,
* 即默认所有 2^32个可能的二进制值都是有效的,出错时返回 INADDR_NONE(通常是32位全1的值),
* 这意味着255.255.255.255不能由此函数处理。
*/
in_addr_t inet_addr(const char* strptr);

/**
* @description 将32位网络序二进制IPv4 转换为点分十进制数串。
*/
char* inet_ntoa(struct in_addr inaddr);
#include <arpa/inet.h>
int main()
{
    struct in_addr addr;
    int ret = inet_aton("192.168.1.126",&addr);
    if(ret)
    {
        printf("192.168.1.126 的网络字节序为: %d\n",addr.s_addr);

        printf("网络字节序: %d,转换为十进制点分数串: %s\n",addr.s_addr,inet_ntoa(addr));
    }

    return 0;
}

在这里插入图片描述
以下函数兼容 IPv4 和 IPv6
family 既可以是 AF_INET 也可以是 AF_INET6

#include <arpa/inet.h>
/**
* @description 将 strptr 所指的字符串转换成32位网络字节序二进制值,并通过 addrptr指针存储
* @return 成功:1;失败:0 
*/
int inet_pton(int family,const char* strptr,void *addrptr);
/**
* @description 将32位网络序二进制IPv4 addrptr 转换为点分十进制数串 strptr。
* len 为 strptr 空间长度,如果这个长度不够则返回nullptr,并置 errno 为 ENOSPC。
* strptr 不可以为空,必须提前分配好空间。 
*/
const char *inet_ntop(int family, const void *addrptr, char* strptr, size_t len);

调用实例

struct in_addr addr;
int ret = inet_pton(AF_INET,"192.168.1.126",(void*)&addr);
if(ret)
{
    printf("192.168.1.126 的网络字节序为: %d\n",addr.s_addr);
}

套接字函数

基本 TCP 客户、服务器程序的套接字函数如下图
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

#include <sys/socket.h>
/**
* 当 protocol 设置为 0 时,选择 family 和 type 组合的系统默认值
* 返回套接字描述符(sockfd), 一个非负整数值
*/
int socket(int family, int type, int protocol);

int connect(int sockfd, const struct *servaddr, socklen_t addrlen);

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);

int listen(int sockfd, int backlog);

int accept(int sockfd, const struct sockaddr* cliaddr, socklen_t *addrlen);

这里需要注意的:
listen 函数中的 backlog 参数,这个是已连接队列和未连接完成队列之和,详细不展开,相关知识在第4章第五节。
accept 是一个阻塞函数,从已连接队列中取出已连接 sockect。

目前掌握的知识只能写演示代码,还不足以商用。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值