网络套接字/TCP/UDP 以及常见API的详解

计算机之间的传输媒介是光信号和电信号,通过“频率“ 和 ”强弱“ 来表示0 和 1 这样的信息 。 要想传递各种不同的信息 ,就要提前约定好双方的数据格式。
协议——————一种双方的事先约定

TCP/IP五层或四层模型

物理层:
物理层负责光信号/电信号的传递方式,
eg:双绞线网线
物理层的能力决定双绞线的最大传输能力,

集线器工作在物理层

数据链路层:负责数据帧的传送和识别。
eg:网卡设备的驱动、祯同步(从网线上检测到什么信号算作帧的开始)
冲突检测(检测到冲突就自动重发)

交换机工作在数据链路层

网络层:负责地址管理和路由选择,
eg:在IP协议中,通过IP地址来识别i一台主机,并通过路由表的方式规划出两台主机之间shu ju chuan数据传输的路由线路

路由器 工作在网络层

传输层:
负责两台主机之间的数据传输,如传输控制协议TCP能确保数据准确的传输

应用层:
负责应用程序之间的沟通,如电子邮件传输,文件传输协议等。

网络编程主要针对应用层

数据包封装和分用

数据包在传输层叫段,
在网络层叫数据报,
在链路层叫帧

应用层数据通过协议栈发送到网络上时,每层协议都要加上一个数据首部,称为封装

首部信息中包含一些类似于首部多长,上层协议是什么等信息

数据封装成帧后 发送到传输介质上,到达目的主机后,每层协议再去掉相应的首部,根据首部中的上层协议字段,将数据交给上层协议处理。

网络中的地址控制

Mac 地址用来识别数据链路层中相邻的节点
长度为48位,(6字节),一般用16进制数字加上冒号来表示
在网卡出厂时就确定了,不能修改。 mac地址通常是唯一的 ,虚拟机中的地址不是真实的Mac地址。

网络编程套接字

认识端口号:

  1. 端口号是2个字节16位的整数
  2. 端口号用来标识一个进程,告诉操作系统,当前的这个数据要交给那一个进程来处理
  3. IP地址 + 端口号能够标识网路 上的某一台主机上的某一个进程
  4. 一个端口号只能被一个进程占用

端口号和进程ID

一个进程能绑定多个端口号,但一个端口号不能同时绑定多个进程

进程号pid相当于 10086 端口号 相当于 内部各个客服

UDP协议
传输层协议
无连接
不可靠传输
面向数据段

TCP协议
传输层协议
有链接
可靠传输
面向字节流

注意:可靠传输和不可靠传输指的是知不知道字节发送数据成功,可靠传输不一定发送成功 ,例如突然断网等

网络字节序

网络字节序是大端字节序。

x86_64 电脑都是小端

内存中的多字节数据相对于内存地址大小有大小端之分,
磁盘文件中的多字节数据数据相对文件中的偏移地址也有大小端之分,
网络数据流同样有大小端之分。

为了使网络程序具有可移植性,可以调用以下库函数做网络字节序和主机字节序的转换

#include <arpa/inet.h>

uint32_t htonl(uint32_t host long);
uint16_t htons(uint16_t short);
uint32_t ntohl(uint32_t netlong);
uint16_t ntos(uint16_t net short);

如果主机是大端字节序,这些函数不做转换,原封不动返回参数,
如果主机是小端字节序,这些函数将参数转换为相应的大小端转换后 返回。

常见的API

//创建文件描述符(TCP/UDP,客户端+服务器)
int socket(int domain , int type , int protocol);

参数:1 地址类型,2 协议类型,3 协议

//绑定端口号 服务器 ,Socket TCP?UDP
int bind(int socket, const struct sockaddr* addr, socklen_t address_len);

//开始监听socket(TCP ,服务器)

int listen(int socket, int backlog);backlog:监听队列长度

//TCP 服务器
int accept(int socket, struct sockaddr* address,socklen_t*address_len);

//建立连接
int connect(int socket,struct sockaddr* addr,soclen_t addrlen);

sockaddr 结构

IPv4 和 IPv6的地址格式分别定义为AF_INET
和 AF_INET6

socket API都可以用 struct sockaddr* 表示,在实用的时候需要强制转化为 sockaddr_in 使用 ,
好处是 程序的通用性。

地址转换函数

字符串转in_addr的函数
#include <arpa/inet.h>

int inet_aton(const char strptr , struct in_addr addrptr );

in_addr_t inet_addr(const char* strptr);

int inet_pton(int family , const char* strptr , void* addrptr);

in_addr 转字符串的函数
char* inet_ntoa(struct in_addr inaddr);

const char* inet_ntop(int famliy, const void* addrptr, char* strptr);

其中,inet_pton 和 ine_ntop 既可以转化ipv4地址,也可以转化ipv6地址,因此参数类型为 void*addrptr类型

关于 inet_ntoa 函数

(该函数不是线程安全函数)

inet_ntoa函数返回值为 char*类型,那么意味着,函数在内部申请了一块内存空间,我们要不要手动释放?
在这里插入图片描述
man 手册说,这个函数的返回结果放到了静态存储区,这个时候不需要我们手动释放,那么如果我们多次调用,会出现什么样的效果呢?

来看代码
在这里插入图片描述

运行结果如下

在这里插入图片描述
我们发现 会覆盖掉上一次的结果

因为inet_ntoa 会把结果放到自己内部的一个静态存储区。

那么问题又来了,多个线程调用inet_ntoa 会不会出问题呢?覆盖掉不该覆盖的数据?

这个问题在APUE中明确提出 ,inet_ntoa 不是线程安全函数 ,但是centos7没有出现问题,可能是因为内部添加了互斥锁吧。

centos7 测试情况如下

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <netinet/in.h>

void* Func1(void* arg)
{
  sockaddr_in* id =  (sockaddr_in*) arg;
  while(1)
  {
    char* ptr = inet_ntoa(id->sin_addr);
    printf("addr1 : %s : \n",ptr);
    sleep(1);
  }

  return NULL;
}


void* Func2(void* arg)
{
  sockaddr_in* id =  (sockaddr_in*) arg;
  while(1)
  {
    char* ptr = inet_ntoa(id->sin_addr);
    printf("addr2 : %s : \n",ptr);
    sleep(1);
  }

  return NULL;
}

int main()
{
  pthread_t t1,t2;

  sockaddr_in addr1;
  sockaddr_in addr2;
  addr1.sin_addr.s_addr = 0;
  addr2.sin_addr.s_addr = 0xffffffff;

  pthread_create(&t1,NULL,Func1,&addr1);
  pthread_create(&t2,NULL,Func2,&addr2);

  pthread_join(t1,NULL);
  pthread_join(t2,NULL);

  return 0;
}

在这里插入图片描述
因此 ,多线程环境下 ,推荐使用 inet_ntop(),这个函数由调用者提供一个缓冲区保存结果,可以避免线程安全问题

TCP网络程序

socket ()
对于TCP协议type参数指定为SOCK_STREAM 表示面向流的传输协议。protocol 指定为0即可

bind()
bind的 作用是将参数socked和 myaddr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号。 myaddr是我们创建的对象 。包含 IP和 port

因为sockaddr是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而他们的长度各不相同,所以需要第三个参数 addrlen来指定结构体的长度

listen ()
listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,收到更多的连接请求就忽略,backlog一般不会设置太大,一般设置为5

listen 成功 0 失败 -1

accept
int accept(int socket,sockaddr* addr,socklen_t* addrlen);

  1. List item三次握手完成后,服务器调用accept 接受连接
  2. 如果服务器调用accept的时候,客户端还没有连接请求,将会发生阻塞式等待
  3. addr是一个传出参数,accept返回时穿出IP和port
  4. 如果给addr参数传NULL,表示不关心客户端的地址
  5. addrlen 参数是一个输入输出型参数,输入是调用者提供的,缓冲区的长度要足够长避免溢出,输出的是客户端的地质结构的实际长度。

accept 成功将会交给connect 去处理,而accept 自身接着去accept。
(后边符accept 的详细解析)

connect ()

  1. 客户端需要调用connect 来连接服务器
  2. connect 和bind的参数形式一致
  3. int connect (int socketfd ,const sockaddr*addr,socklen_t addrlen);
  4. 成功 返回 0 失败 -1

aceept再解析

本函数从s的等待连接队列中抽取第一个连接,创建一个与s同类的新的套接口并返回句柄。如果队列中无等待连接,且套接口为阻塞方式,则accept()阻塞调用进程直至新的连接出现。如果套接口为非阻塞方式且队列中无等待连接,则accept()返回一错误代码。已接受连接的套接口不能用于接受新的连接,原套接口仍保持开放。

addr参数为一个返回参数,其中填写的是为通讯层所知的连接实体地址。addr参数的实际格式由通讯时产生的地址族确定。addrlen参数也是一个返回参数,在调用时初始化为addr所指的地址空间;在调用结束时它包含了实际返回的地址的长度(用字节数表示)。该函数与SOCK_STREAM类型的面向连接的套接口一起使用。如果addr与addrlen中有一个为零NULL,将不返回所接受的套接口远程地址的任何信息。

返回值:

如果没有错误产生,则accept()返回一个描述所接受包的SOCKET类型的值。否则的话,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码。

addrlen所指的整形数初始时包含addr所指地址空间的大小,在返回时它包含实际返回地址的字节长度。

服务器初始化:

  1. 调用socket ,创建
  2. 调用bind,绑定服务器的IP地址,端口号和文件描述符。
  3. 调用listen。声明当前这个文件描述符作为一个服务器的文件描述符,为后边的accept 做好准备。
  4. 调用accept并阻塞,等待客户端连接

UDP的数据传输流程:
服务器
1 创建套接字
2 绑定套接字
3 接收请求
4 发送响应
5 关闭套接字

客户端
1 创建套接字
2 绑定套接字
3 发送请求
4 接收回应
5 关闭套接字

socket 都做了什么?

,创建套接字,其实是在内核中创建了一个
socket struct ,这个结构体中维护着多个数据,其中有两项是读缓冲区,和写缓冲区,还有其他。

操作系统的责任就是将网卡收到的数进行端口辨认,然后去找不同的socket自己的缓冲区 ,用户起诉就是从缓冲区当中读写数据。
类似于我们买的迅游加速器。

创建套接字
int socket(int domain ,int type ,int protocol);

domain :地址屿
AF_INET
AF_INET6

type :套接字的类型

SOCK_DGRAM  :  面向数据包
SOCK_STREAM  :面向字节流  

protocol : 协议类型
0,代表使用默认协议,其实0来源的协议为type 相关默认的协议 ,
假如我们不知道,可以使用0,但更好的用法是精确用法:
TCP:IPPROTO_TCP 或者 6
UDP:IPPROTO_UDP 或者17
返回值 socket 就是
套接字的句柄,实质是一个文件描述符。

struct sockaddr
{
sa_family_t sa_family;//填充地址屿, 2个字节
char sa【14】;//
}

TCP三次握手

1 建立连接

  1. 调用socket,创建fd

  2. 调用connect,向服务器发起连接请求

  3. connect会发出SYN段,并阻塞等待服务器应答(第一次)

  4. 服务器收到客户端的SYN,会应答一个确认收到即SYN+ACK段来表示 “同意建立连接”(第二次)

  5. 客户端收到服务器的SYN+ACK后,会从connect()返回,同时向服务器应答一个ACK,确认收到(第三次)

TCP客户端和服务器建立连接的过程,称为三次握手

2 数据传输过程

1.,TCP协议提供全双工的通信服务;全双工(同一时刻,同一连接中通信双方可同时写数据)

  1. 建立连接后,服务器从accept()返回返回后立刻调用read(),读socket就像管道一样,如果数据未到达就阻塞等待到达就从read()返回

  2. 服务器阻塞等待时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理。服务器在处理客户端请求期间,客户端也调用read阻塞等待服务器的应答.

服务器调用write()将处理的结果发送给客户端后,继续调用read()阻塞式等待客户端的下一条请求。

  1. 客户端从read()返回,继续发送下一条数据,这样循环下去,否则准备断开连接(四次挥手)。

四次挥手

3 断开连接

  1. 如果客户端没有更多的请求,就调用close()关闭连接,此时客户端会向服务器发送FIN(第一次)

  2. 此是服务器收到FIN后,会回应一个ACK,同时read()返回0 (第二次)

  3. read()返回之后,服务器就知道客户端关闭了连接,也调用close()关闭连接,这个时候服务器会向客户端发送一个FIN(第三次)

  4. 客户端收到FIN,最后再返回一个ACK给服务器(第四次)

断开连接的过程称为四次挥手

要清楚:read()是读请求的,write()是用来读响应的。

connect函数和TCP交互是通过发出SYN段
read()返回,说明收到了FIN 段

connect成功之后,
服务器
accept返回,并分配新的文件描述符和客户端通信。
read(fd_,buf,size)阻塞等待客户端数据请求
write(fd,buf,size)发送数据应答。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值