网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程

目录
第一章 理解网络编程和套接字
1.1 网络编程和套接字概要
1.2 基于Linux的文件操作
1.2.1 底层文件访问和文件描述符
1.2.2 打开文件
1.2.3 关闭文件
1.2.4 将数据写入文件
1.2.5 读取文件中的数据
第二章 套接字类型与协议设置
2.1 套接字协议及其数据传输特性
2.1.1 关于协议
2.1.2 创建套接字
第三章 地址族与数据序列
3.1 分配给套接字的IP地址与端口号
3.1.1 网络地址
3.1.2 用于区分套接字的端口号
【网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程】3.2 地址信息的表示
3.2.1 表示IPv4地址的结构体
3.3 网络字节序与地址变换
3.3.1 字节序与网络字节序
3.3.2 字节序转换
3.4 网络地址的初始化与分配
3.4.1 将字符串信息转换为网络字节序的整数型
3.4.2 网络地址初始化
3.4.3 INADDR_ANY
3.4.4 向套接字分配网络地址
第四章 基于TCP的服务器端
4.1 理解TCP和UDP
4.1.1 TCP/IP协议栈
4.2 实现基于TCP的服务器端/客户端
4.2.1 TCP服务器端的默认函数调用顺序
4.2.2 服务端进入等待连接请求阶段
4.2.3 服务端受理客户端连接请求
4.2.4 TCP客户端的默认函数调用顺序
4.2.5 基于TCP的服务器端/客户端函数调用关系
4.3 实现迭代服务器端/客户端
4.3.1 实现迭代服务器端
第五章 TCP原理
5.1 TCP套接字中的I/O缓冲
5.2 TCP内部工作原理1:与对方套接字的连接
5.3 TCP内部工作原理2:与对方主机的数据交换
5.4 TCP的内部工作原理3:断开与套接字的连接
第一章 理解网络编程和套接字 1.1 网络编程和套接字概要 网络编程就是编写程序使两台连网的计算机相互交换数据。首先需要的是物理连接,目前大部分计算机都连接到庞大的互联网中;在此基础上,只需考虑如何编写数据传输软件,而操作系统为我们提供了名为"套接字"的部件,套接字是数据网络传输用的软件设备。网络编程又称为套接字编程。
服务器端是能够受理连接请求的程序,服务器端创建的套接字称为服务器端套接字或监听套接字;客户端是用于请求连接的,客户端创建的套接字称为客户端套接字。
1.2 基于Linux的文件操作 在Linux系统中,套接字socket也被认为是文件的一种,因此在网络传输过程中可以使用文件I/O的相关函数;而Windows系统于Linux不同,要区分socket和文件,因此两种系统中的编程方式也不相同。之后主要学习的是Linux的编程方式。
1.2.1 底层文件访问和文件描述符
底层指的是与标准无关的操作系统独立提供的。文件描述符是系统分配给文件或套接字的整数,这个整数将成为程序员与操作系统之间良好沟通的渠道,是为了方便称呼操作系统创建的文件或套接字而赋予的数。有一些文件描述符是固定的,比如C语言中学习的标准输入输出及标准错误,即描述符从3开始由小到大的顺序编号,因为0、1、2是分配给标准I/O的描述符。

文件描述符 对象
0 标准输入:Standard Input
1 标准输出:Standard Output
2 标准错误:Standard Error
文件和套接字一般经过创建过程才会被分配文件描述符。
1.2.2 打开文件
#include
#include
#include

int open(const char *path, int flag); //成功时返回文件描述符,失败时返回-1
  • path:文件名的字符串地址
  • flag:文件打开模式信息
文件打开模式flag可能的常量值和含义:
打开模式 含义
O_CREAT 必要时创建文件
O_TRUNG 删除全部现有数据
O_APPEND 维持现有数据,保存到其后面
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWD 读写打开
1.2.3 关闭文件
#include

int close(int fd); //成功时返回0,失败时返回-1
  • fd:需关闭的文件或套接字的文件描述符
此函数不仅可以关闭文件,也可以关闭套接字。
1.2.4 将数据写入文件
#include

ssize_t write(int fd,const void *buf,size_t nbytes); //成功时返回写入的字节数,失败时返回-1
  • ssize_t:signed intsize_t:unsigned int使用typedef声明的两个数据类型
  • fd:显示数据传输对象的文件描述符
  • buf:保存要传输数据的缓存地址值
  • nbytes:要传输数据的字节数
用于向文件输出(传输数据);通过套接字向其他计算机传递数据。
1.2.5 读取文件中的数据
#include

ssize_t read(int fd, void *buf,size_t nbytes);
  • fd:显示数据传输对象的文件描述符
  • buf:保存要接收数据的缓存地址值
  • nbytes:要接收数据的最大字节数
第二章 套接字类型与协议设置 2.1 套接字协议及其数据传输特性 2.1.1 关于协议
协议是对话中使用的通信规则,在计算机领域内理解为计算机间对话必备通信规则。
2.1.2 创建套接字
#include

int socket(int domain, int type, int protocol); //成功时返回文件描述符,失败时返回-1
  • domain:套接字中使用的协议族信息
  • type:套接字数据传输类型信息
  • protocol:计算机间通信中使用的协议信息
头文件sys/socket.h中声明的协议族:
名称 协议族
PF_INET IPv4互联网协议族
PF_INET6 IPv6互联网协议族
PF_LOCAL 本地通信的UNIX协议族
PF_PACKET 底层套接字的协议族
PF_IPX IPX Novell协议族
套接字类型Type指的是套接字的数据传输方式,因为决定了协议族并不能同时决定数据传输方式,一个协议族内存在多种数据传输方式:
  • SOCK_STREAM:面向连接的套接字。
    • 特征:
      • 传输过程中数据不会消失
      • 按序传输数据
      • 传输的数据不存在数据边界
    • 传输的数据不存在数据边界,指的是收发数据的套接字内部有缓冲(字节数组),因此收到数据并不意味着马上调用read函数,只要不超过缓冲容量,则有可能在数据填充满缓冲后1次read函数调用读取全部,也有可能分成多次read函数调用进行读取。在面向连接的套接字中,read函数和write函数的调用次数并无太大意义
    • 面向连接的套接字连接必须一一对应,只能与另外一个同样特性的套接字连接
    • 可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字
  • SOCK_DGRAM:面向消息的套接字。
    • 特征:
      • 强调快速传输而非传输顺序
      • 传输的数据可能丢失也可能损毁
      • 传输的数据有数据边界
      • 限制每次传输的数据大小
    • 接收数据的次数应和传输次数相同
    • 不可靠的、不按序传递的、以数据的高速传输为目的的套接字
第三个参数决定最终采用的协议。除非遇到同一协议族中存在多个数据传输方式相同的协议,大部分情况下可以向第三个参数传递0。
常用调用方式:
int tcp_socket = socket(PF_INET, SOCK_STREAM, 0);
0指的是满足PF_INET和SOCK_STREAM条件的协议IPPROTO_TCP
int udp_socket = socket(PF_INET, SOCK_DGRAM, 0);
0指的是满足PF_INET和SOCK_DGRAM条件的协议IPPROTO_UDP
第三章 地址族与数据序列 3.1 分配给套接字的IP地址与端口号 IP(Internet Protocol),是为收发网络数据而分配给计算机的值。端口号是为区分程序中窗口的套接字而分配给套接字的符号。
3.1.1 网络地址
IP地址分为两类:
IPv4:4字节地址族
IPv6:16字节地址族
IPv4标准的4字节IP地址分为网络地址和主机(指计算机)地址,分为A、B、C、D、E类型。数据传输时先根据网络地址找到计算机所属的局域网,再根据主机地址找到该局域网下的那个计算机。向相应网络传输数据实际上是向构成网络的路由器或交换机传递数据,由接收数据的路由器根据数据中的主机地址向目标主机传递数据。
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

通过IP地址的第一个字节判断网络地址占用的字节数,即判断所属类型:
  • A类地址的首字节范围:0~127
  • B类地址的首字节范围:128~191
  • C类地址的首字节范围:192~223
3.1.2 用于区分套接字的端口号
IP用于区分计算机,只要有IP地址就能向目标主机传输数据,但仅凭这些无法传输给最终的应用程序。计算机中一般配有NIC(网络接口卡)数据传输设备,通过NIC向计算机内部传输数据时会用到IP。操作系统负责把传递到内部的数据适当分配给套接字,这是就要利用端口号。即通过NIC接收的数据内有端口号,操作系统正是参考此端口号把数据传输给相应端口的套接字。
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

端口号就是在同一操作系统内为区分不同套接字而设置的,不能将1个端口号分配给不同的套接字。端口号由16位构成,可分配的端口号范围是0-65535。注意:虽然端口号不能重复,但TCP套接字和UDP套接字不会共用端口号,所以允许重复。
3.2 地址信息的表示 3.2.1 表示IPv4地址的结构体
struct sockaddr_in{
sa_family_tsin_family; //地址族
uint16_tsin_port; //16位TCP/UDP端口号
struct in_addrsin_addr; //32位ip地址
charsin_zero[8]; //不使用
}
struct in_addr{
In_addr_ts_addr; //32位IPv4地址
}
这些数据类型参考:
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

  • 成员sin_family:每种协议族适用的地址族均不同。
地址族 含义
AF_INET IPv4网络协议中使用的地址族
AF_INET6 IPv6网络协议中使用的地址族
AF_LOCAL 本地通讯中采用的UNIX协议的地址族
  • 成员sin_port:保存16位端口号,重点在于以网络字节序保存。
  • 成员sin_addr:保存32位IP地址信息,也以网络字节序保存。
  • 成员sin_zero:无特殊含义,只是为使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员,必须填充为0。
struct sockaddr{
sa_family_tsin_family; //地址族
charsa_data[14]; //地址信息
}
  • sa_data保存的地址信息中需包含IP地址和端口号,剩余部分应应填充0。
3.3 网络字节序与地址变换 3.3.1 字节序与网络字节序
CPU向内存保存数据的方式有两种:
  • 大端序:高位字节存放在低位地址
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

  • 小端序:高位字节存放在高位地址
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

代表CPU数据保存方式的主机字节序在不同CPU中也各不相同(目前主流的Intel系列CPU以小端序方式保存数据),因此如果两台计算机的CPU数据保存方式不同,会导致传送的数据和接收后解析的数据不相同。
正因如此,在通过网络传输数据时约定统一方式,这种约定称为网络字节序,其实非常简单,就是统一为大端序,先把数据数组转化成大端序格式再进行网络传输。
3.3.2 字节序转换
转换字节序的函数:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsighed long htonl(unsigned long);
unsigned long ntohl(unsigned long);
  • h代表主机host字节序
  • n代表网络network字节序
  • s代表short类型,用于端口号转换
  • l代表long类型(Linux中long类型占用4个字节),用于IP地址转换
注意:数据的收发过程中有自动转换机制,因此除了向sockaddr_in结构体变量填充数据外,其他情况无需考虑字节序问题。
3.4 网络地址的初始化与分配 3.4.1 将字符串信息转换为网络字节序的整数型
sockaddr_in中保存地址信息的成员为32位整数型,因此需要把点分十进制表示法形式的IP地址转换成32位整数型数据。这个函数在转换类型的同时进行网络字节序转换。
#include

in_addr_t inet_addr(const char *string); //成功时返回32位大端序整数型值,失败时返回INADDR_NONE

int inet_aton(const char *string, struct in_addr *addr); //成功时返回1,失败时返回0
  • string:含有需转换的IP地址信息的字符串地址值
  • addr:将保存转换结果的in_addr结构体变量的地址值
调用inet_addr函数,需将转换后的IP地址信息带入sockaddr_in结构体中声明的in_addr结构体变量。而inet_aton函数不需要此过程,原因在于,若传递in_addr结构体变量值,函数会自动把结果传入该结构体变量。
#include
char* inet_ntoa(struct in_addr adr); //成功时返回转换的字符串地址值,失败时返回-1
此函数可以把网络字节序整数型IP地址转换为字符串形式。这个函数在内部申请了内存并保存了字符串,因此调用完该函数后,应立即将字符串信息复制到其他内存空间,防止再次调用该函数产生的覆盖问题。
3.4.2 网络地址初始化
常见的网络地址信息初始化方法:
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

3.4.3 INADDR_ANY
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

利用常数INADDR_ANY分配服务器的IP地址,采用这种方法,可以以自动获取运行服务器端的计算机IP地址。
3.4.4 向套接字分配网络地址
#include
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen); //成功时返回0,失败时返回-1
  • sockfd:要分配地址信息(IP地址和端口号)的套接字文件描述符
  • myaddr:存有服务端地址信息的结构体变量地址值
  • addrlen:第二个结构体变量的长度
常见的套接字初始化过程:
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

第四章 基于TCP的服务器端 4.1 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字。
TCP套接字是面向连接的,又称基于流的套接字。TCP是Transmission Control Protocol (传输控制协议),意为对数据传输过程的控制。
4.1.1 TCP/IP协议栈
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

IP本身是面向消息的、不可靠的协议,用于路径的选择,如果传输中发送路径错误,则选择其他路径,如果发生数据丢失或错误,则无法解决。TCP和UDP层以IP层提供的路径信息为基础完成实际的数据传输。其中TCP在数据交换过程中可以确认对方已收到数据,并重传丢失的数据,因此TCP协议确认后向不可靠的IP协议赋予可靠性。
根据程序特点决定服务器端和客户端之间的数据传输规则(规定),这就是应用层协议。
4.2 实现基于TCP的服务器端/客户端 4.2.1 TCP服务器端的默认函数调用顺序
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

4.2.2 服务端进入等待连接请求阶段
调用bind函数给套接字分配了地址,接下来通过调用listen函数进入等待连接请求状态。只有服务端调用了listen函数,客户端才能进入可发生连接请求的状态(此时客户端才能调用connect函数)。
#include

int listen(int sock,int backlog); //成功时返回0,失败时返回-1
  • sock:希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字)
  • backlog:连接请求等待队列的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列
客户端请求连接时,服务端受理连接前一直使请求处于等待状态。
4.2.3 服务端受理客户端连接请求
调用listen函数后,若有新的连接请求,则应按序受理。受理请求意味着进入可接收数据的状态。此时除了listen创建出的套接字外,还需要另外一个套接字,来连接到发起请求的客户端,接受数据并写回数据,但是这个套接字不需要自己手动创建,下面这个函数会自动创建套接字。
#include

int accept(int sock, struct sockaddr *addr, socklen_t *addrlen); //成功时返回创建的套接字文件描述符,失败时返回-1
  • sock:服务器套接字的文件描述符
  • addr:保存发起请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息
  • addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完后,该变量即被填入客户端地址长度
accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符,需要强调的是套接字是自动创建的,并自动与发起连接请求的客户端建立连接。
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

4.2.4 TCP客户端的默认函数调用顺序
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

创建客户端套接字后向服务器端发起请求连接。
#include
int connect(int sock, struct sockaddr * servaddr, socklen_t addrlen); //成功时返回0,失败时返回-1
  • sock:客户端套接字文件描述符
  • servaddr:保存目标服务器端地址信息的变量地址值
  • addrlen:以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
客户端调用connect函数后,发生以下情况之一才会返回(完成函数调用):
  • 服务器端接收连接请求
    • 注意:接收连接请求并不意味着服务器端调用accept函数,而是服务器端把连接请求信息记录到等待队列
  • 发生断网等异常情况而中断连接请求
客户端套接字何时、何地、如何分配地址信息?
何时:调用connect函数时
何地:操作系统内核中
如何:IP用计算机(主机)的IP,端口随机
4.2.5 基于TCP的服务器端/客户端函数调用关系
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

注意:
  • 客户端只能等到服务器调用listen函数后才能调用connect函数
  • 客户端调用connect函数前,服务器端有可能率先调用accept函数(此时服务器端在调用accept函数时进入阻塞状态,直到客户端调用connect函数为止)
4.3 实现迭代服务器端/客户端 4.3.1 实现迭代服务器端
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

服务器端在同一时刻只能服务于一个客户端,即只能连接一个客户端,因此每一次都要重新调用accept函数。
  • 但是可能存在一个问题:由于TCP是面向连接的套接字,不存在数据边界,客户端多次调用write函数传递的字符串有可能一次性传递到服务器端,此时客户端可能同时把这些字符串接收到,导致不满足我们的期望;也有可能客户端还没有收到全部数据包就调用了read函数。
  • 解决方法:应用层协议的定义,收发数据时要根据期望要求定义好规则(协议)来表示数据的边界。
第五章 TCP原理 5.1 TCP套接字中的I/O缓冲 write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据,而是write函数调用瞬间,数据将移至输出缓冲,read函数调用瞬间,从输入缓冲读取数据。
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

  • I/O缓冲在每个TCP套接字中单独存在
  • I/O缓冲中在创建套接字时自动生成
  • 即使关闭套接字也会继续传递输出缓冲中遗留的数据
  • 关闭套接字将丢失输入缓冲中的数据
TCP中有滑动窗口协议,会控制数据流(流量控制),不会发生超过输入缓冲大小的数据传输。因此TCP中不会因为缓冲溢出而丢失数据。
TCP套接字从创建到消失过程:
  • 与对方套接字建立连接
  • 与对方套接字进行数据交换
  • 断开与对方套接字的连接
5.2 TCP内部工作原理1:与对方套接字的连接 TCP在实际通信过程中会经过三次对话过程,三次握手。
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

套接字是以双全工方式工作的,可以双向传递数据。
TCP连接收发数据前都会向数据包分配序号,并向对方通报此序号,防止数据丢失。
5.3 TCP内部工作原理2:与对方主机的数据交换 网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

每一次ACK号的增量为传输的数据字节数。
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片

5.4 TCP的内部工作原理3:断开与套接字的连接 断开连接时也需要双方协商,四次握手。
网络编程|TCP/IP网络编程---Linux系统下的TCP套接字编程
文章图片



PS:参考书籍《TCP/IP网络编程》 尹圣雨著 归纳整理

    推荐阅读