网络编程

本文介绍了网络编程中的客户端-服务器端模型,阐述了IP地址、端口号、保留网段和子网掩码的概念。详细解析了TCP/IP连接的建立、数据传输和关闭过程,以及套接字在其中的作用。同时,提到了IPv4和IPv6的套接字地址格式,并探讨了本地和远程socket的区别。最后,讲解了socket的历史及其在不同操作系统中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

网络编程模型:客户端-服务器端

无论是客户端,还是服务器端,它们运行的单位都是进程(process),而不是机器。一个客户端,比如手机终端,同一个时刻可以建立多个到不同服务器的连接,比如同时打游戏,上知乎,逛天猫;而服务器端更是可能在一台机器上部署运行了多个服务,比如同时开启了SSH服务和HTTP服务。

端口和IP

酒店的地址是唯一的,每间房间的号码是不同的,类似的,计算机的IP地址是唯一的,每个连接的端口号是不同的。

端口号是一个16位的整数,最多为65536。当一个客户端发起连接请求时,客户端的端口是由操作系统内核临时分配的,称为临时端口;然而,前面也提到过,服务器端的端口通常是一个众所周知的端口。

一个连接可以通过客户端-服务器端的IP和端口唯一确定,这叫做套接字对,按照下面的四元组表示:

(clientaddr:clientport, serveraddr: serverport)

保留网段

国际标准组织在IPv4地址空间里面,专门划出了一些网段,这些网段不会用做公网上的IP,而是仅仅保留做内部使用,因此把这些地址称作保留网段。如:10.0.x.x或者192.168.x.x

子网掩码

IP划分:

  1. 网络:表示的是这组IP共同的部分,比如在192.168.1.1~192.168.1.255这个区间里,它们共同的部分是192.168.1.0。
  2. 主机:表示的是这组IP不同的部分,上面的例子中1~255就是不同的那些部分,表示有255个可用的不同IP
    192.0.2.12,可以说前面三个 bytes 是子网,最后一个 byte 是 host,或者换个方式,能说 host 为8 位,子网掩码为192.0.2.0/24(255.255.255.0)。
    子网掩码还需要满足一个条件才可以使用:它的二进制中1和0必须是连续的。

子网掩码就是用来遮掩IP地址并划分网段的工具,根据遮掩的位数不同来划分不同的网段。

192.0.2.12/30, 这样就很容易知道有30个1, 2个0,所以主机个数为4

全球域名系统

数据报和字节流

TCP,又被叫做字节流套接字(Stream Socket),注意这里先引入套接字socket,套接字socket实际上是网络编程的核心概念。

DP也有一个类似的叫法,数据报套接字(Datagram Socket),无连接的Sockets
一般分别以“SOCK_STREAM”与“SOCK_DGRAM”分别来表示TCP和UDP套接字。

UDP在很多场景也得到了极大的应用,比如多人联网游戏、视频会议,甚至聊天室,NTP。

UDP也可以做到更高的可靠性,只不过这种可靠性,需要应用程序进行设计处理,比如对报文进行编号,设计Request-Ack机制,再加上重传等,在一定程度上可以达到更为高可靠的UDP程序。

套接字和地址

socket的寓意是可以通过插口接入的方式,快速完成网络连接和数据收发。

下图是客户端和服务器工作的核心逻辑:

先从右侧的服务器端开始看,因为在客户端发起连接请求之前,服务器端必须初始化好。右侧的图显示的是服务器端初始化的过程,首先初始化socket,之后服务器端需要执行bind函数,将自己的服务能力绑定在一个众所周知的地址和端口上,紧接着,服务器端执行listen操作,将原先的socket转化为服务端的socket,服务端最后阻塞在accept上等待客户端请求的到来。

此时,服务器端已经准备就绪。客户端需要先初始化socket,再执行connect向服务器端的地址和端口发起连接请求,这里的地址和端口必须是客户端预先知晓的。这个过程,就是著名的TCP三次握手(Three-way Handshake)。一旦三次握手完成,客户端和服务器端建立连接,就进入了数据传输过程。

具体来说,客户端进程向操作系统内核发起write字节流写操作,内核协议栈将字节流通过网络设备传输到服务器端,服务器端从内核得到信息,将字节流从内核读入到进程中,并开始业务逻辑的处理,完成之后,服务器端再将得到的结果以同样的方式写给客户端。可以看到,一旦连接建立,数据的传输就不再是单向的,而是双向的,这也是TCP的一个显著特性。

当客户端完成和服务器端的交互后,比如执行一次Telnet操作,或者一次HTTP请求,需要和服务器端断开连接时,就会执行close函数,操作系统内核此时会通过原先的连接链路向服务器端发送一个FIN包,服务器收到之后执行被动关闭,这时候整个链路处于半关闭状态,此后,服务器端也会执行close函数,整个链路才会真正关闭。半关闭的状态下,发起close请求的一方在没有收到对方FIN包之前都认为连接是正常的;而在全关闭的状态下,双方都感知连接已经关闭。

以上所有的操作,都是通过socket来完成的。无论是客户端的connect,还是服务端的accept,或者read/write操作等,socket是用来建立连接,传输数据的唯一途径。

理解socket

可以把整个TCP的网络交互和数据传输想象成打电话,顺着这个思路想象,socket就好像是手里的电话机,connect就好比拿着电话机拨号,而服务器端的bind就好比是去电信公司开户,将电话号码和家里的电话机绑定,这样别人就可以用这个号码找到你,listen就好似人们在家里听到了响铃,accept就好比是被叫的一方拿起电话开始应答。至此,三次握手就完成了,连接建立完毕。

接下来,拨打电话的人开始说话:“你好。”这时就进入了write,接收电话的人听到的过程可以想象成read(听到并读出数据),并且开始应答,双方就进入了read/write的数据传输过程。

最后,拨打电话的人完成了此次交流,挂上电话,对应的操作可以理解为close,接听电话的人知道对方已挂机,也挂上电话,也是一次close。

socket发展历史

socket是加州大学伯克利分校的研究人员在20世纪80年代早期提出的,所以也被叫做伯克利套接字。
其提出是为了想用socket的概念,屏蔽掉底层协议栈的差别。第一版实现socket的就是TCP/IP协议,最早是在BSD 4.2 Unix内核上实现了socket。Linux作为Unix系统的一个开源实现,很早就从头开发实现了TCP/IP协议,伴随着socket的成功,Windows也引入了socket的概念。于是在今天的世界里,socket成为网络互联互通的标准。

套接字地址格式

通用套接字地址格式
/* POSIX.1g 规范规定了地址族为2字节的值.  */
typedef unsigned short int sa_family_t;
/* 描述通用套接字地址  */
struct sockaddr{
    sa_family_t sa_family;  /* 地址族.  16-bit 表示使用什么样的方式对地址进行解释和保存,
                            好比电话簿里的手机格式,或者是固话格式,这两种格式的长度和含义都是不同的。*/
    char sa_data[14];   /* 具体的地址值 112-bit */
  };

地址族在glibc里的定义非常多,常用的有以下几种:

  • AF_LOCAL:表示的是本地地址,对应的是Unix套接字,这种情况一般用于本地socket通信,很多情况下也可以写成AF_UNIX、AF_FILE;
  • AF_INET:因特网使用的IPv4地址;
  • AF_INET6:因特网使用的IPv6地址。

AF_表示的含义是Address Family,但是很多情况下,也会看到以PF_表示的宏,比如PF_INET、PF_INET6等,实际上PF_的意思是Protocol Family,也就是协议族的意思。用AF_xxx这样的值来初始化socket地址,用PF_xxx这样的值来初始化socket。

IPv4套接字格式地址
/* IPV4套接字地址,32bit值.  */
typedef uint32_t in_addr_t;
struct in_addr
  {
    in_addr_t s_addr;
  };
  
/* 描述IPV4的套接字地址格式  */
struct sockaddr_in
  {
    sa_family_t sin_family; /* 16-bit 就是AF_INET*/
    in_port_t sin_port;     /* 端口口  16-bit*/
    struct in_addr sin_addr;    /* Internet address. 32-bit */


    /* 这里仅仅用作占位符,不做实际用处  */
    unsigned char sin_zero[8];
  };

可以看到端口号最多是16-bit,也就是说最大支持2的16次方,这个数字是65536,所以支持寻址的端口号最多就是65535。保留端口就是大家约定俗成的,已经被对应服务广为使用的端口,比如ftp的21端口,ssh的22端口,http的80端口等。一般而言,大于5000的端口可以作为自己应用程序的端口使用。

实际的IPv4地址是一个32-bit的字段,可以想象最多支持的地址数就是2的32次方,大约是42亿,应该说这个数字在设计之初还是非常巨大的,无奈互联网蓬勃发展,全球接入的设备越来越多,这个数字渐渐显得不太够用了,于是大家所熟知的IPv6就隆重登场了。

IPv6套接字地址格式
struct sockaddr_in6
  {
    sa_family_t sin6_family; /* 16-bit */
    in_port_t sin6_port;  /* 传输端口号 # 16-bit */
    uint32_t sin6_flowinfo; /* IPv6流控信息 32-bit*/
    struct in6_addr sin6_addr;  /* IPv6地址128-bit */
    uint32_t sin6_scope_id; /* IPv6域ID 32-bit */
  };

整个结构体长度是28个字节,其中流控信息和域IP先不用管,这两个字段,一个在glibc的官网上根本没出现,另一个是当前未使用的字段。这里的地址族显然应该是AF_INET6,端口同IPv4地址一样,关键的地址从32位升级到128位,完全解决了寻址数字不够的问题。

请注意,以上无论IPv4还是IPv6的地址格式都是因特网套接字的格式,还有一种本地套接字格式,用来做为本地进程间的通信, 也就是前面提到的AF_LOCAL。

struct sockaddr_un {
    unsigned short sun_family; /* 固定为 AF_LOCAL */
    char sun_path[108];   /* 路径名 */
};

unix系统有一种一统天下的简洁之美:一切皆文件,socket也是文件。

  1. 像sock_addr的结构体里描述的那样,几种套接字都要有地址族和地址两个字段。这容易理解,你要与外部通信,肯定要至少告诉计算机对方的地址和使用的是哪一种地址。与远程计算机的通信还需要一个端口号。

  2. 本地socket本质上是在访问本地的文件系统,所以自然不需要端口。远程socket是直接将一段字节流发送到远程计算机的一个进程,而远程计算机可能同时有多个进程在监听,所以用端口号标定要发给哪一个进程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值