学好套接字,不单单只是学习相应的接口函数、协议概念。你需要对前面进程、线程、线程池、文件描述符等一些比较重要的知识点掌握的比较扎实才能够体会到前后知识点贯穿的重要性,才能感受到套接字学习给你带来的巨大收获。如果你在阅读本篇博客的时候对前面的知识点不是很熟悉,以下三个链接可以去学习或回顾以下;
1. 理解源IP地址和目的IP地址
因特网中的每一台主机都有自己的IP地址,如果要实现A主机与B主机进行通信,A主机就必须要知道B主机的IP地址(目的IP),这样A主机才能向B主机发生数据;B主机接收到数据后,显然也需要A主机一个响应,那么B主机就必须要知道A主机的IP地址(源IP地址);当然这里只是大概的意思,等我们对套接字编程有了一定的了解,知道两个主机之间是如何通信的,再回过头来去理解这些协议的作用和更深层的理解;
2. 理解源端口号和目的端口号
数据链路和IP中的地址,分别是MAC地址和IP地址。前者是用来识别同一链路中不同的计算机,后者是用来识别TCP/IP网络中互连的主机和路由器。在传输层中也有这样类似于地址的概念,那就是端口号。端口号是用来识别同一台计算机中进行通信的不同应用程序。因此,它也被称为程序地址。
如上图所示:一台计算机可能有多个程序。例如接受WWW服务的Web浏览器、电邮客户端、远程登录用的ssh客户端等程序都可以同时运行。传输层协议正是利用这些端口号识别本机中正在进行通信的应用程序,并准确将数据传输。
3. 通过IP地址、端口号、协议号进行通信识别
如下图所示:①和②的通信是在两台计算机上进行的。它们的目标端口号相同,都是80。例如打开两个Web浏览器,同时访问服务器上的两个页面,就会在这个浏览器跟服务器之间产生类似前面的两个通信。在这种情况下必须严格区分这两个通信。因此可以源端口号加以区分。
下图中③和①的目标端口号和源端口号完全相同,但是它们各自的源IP地址不同。此外,还有一种情况上图中并未列出,那就是IP地址和端口完全都一样,只是协议号(表示上层是TCP或UDP的一种编号)。这种情况下,也会认为是两个不同的通信。
因此,TCP/IP或UDP/IP通信中通常采用5个信息来识别一个通信(这个信息在xshell中可以输入netstat -n 命令显示)。他们是“源IP地址”、“目标IP地址”、“协议号”、“源端口号”、“目标端口号”。只要有一项不同就被认为是其他通信。
IP地址最大的意义在于指导一个报文该如何进行路径选择,到哪里去就是去找目标IP地址。端口号的意义在于唯一的标识一台机器上的唯一一个进程。IP地址 + 端口号 = 能够标识互联网中的唯一一个进程!(也就是我们接下来讲的套接字);IP地址 + port(端口号) = socket(套接字)
我们都知道所有进程都需要一个PID来进行表示,但是不是所有的进程都是网络进程,所以不是所有进程都需要端口号。同时一个进程可以绑定多个端口号(就像学生在学校可以有学号,在健身房可以有会员号),但是一个端口号不能被多个进程绑定。
4. 认识TCP协议和UDP协议 1.UDP的特点及目的
TCP与UDP的区别相当大。它充分地实现了数据传输时的各种控制功能,可以进行丢包时重发控制,还可以对次序乱掉的分包进行顺序控制。而这些再UDP中都没有。此外,TCP作为一种面向有连接的协议,只要在确认通信对端存在时才会发生数据,从而可以控制通信流量的浪费。
5. 网络字节序
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?
1. 大端字节序和小端字节序的回顾 2. 字节序
与同一台计算机上的进程进行通信时,一般不考虑字节序。字节序是一个处理器架构特性,用于指示像整数这样的大数据类型内部的字节如何排序。但如果涉及网络通信,那就必须考虑大小端的问题,否则对端主机识别出来的数据可能与发送端想要发送的数据是不一致的。
TCP/IP协议栈使用大端字节序。应用程序交换格式化数据时,字节序问题就会出现。对TCP/IP,地址用网络字节序来表示,所以应用程序有时候需要在处理器的字节序与网络字节序之间进行转换。以确保数据的一致性。
1. inet_aton函数
该函数将strptr所指C字符串转换成一个32位的网络字节序二进制值,并且通过指针addrptr来存储。若成功则返回1,否则返回0;
如果addrptr指针为空,那么该函数仍然对输入的字符串执行有效检查,但不存储任何结果。
2.inet_ntoa函数
该函数讲一个32位的网络字节序二进制IPV4地址转换成相应的点分十进制数串。由该函数的返回值所指向的字符串驻留在静态内存中。
3.inet_addr函数
与inet_aton一样进行相同的转换,返回值为32的网络字节序二进制值。
二、socket编程接口 1. socket常见的API
//创建套接字
int socket(int domain, int type, int protocol);
//绑定端口
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
//监听套接字
int listen(int sockfd, int backlog);
//接受请求
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
//建立连接
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
1. 创建套接字
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
为了执行网络I/O,一个进程必须做的第一件事就是调用socket函数(本质就是打开网络文件),指定期望通信的协议类型(使用IPV4的TCP、使用IPV6的UDP、Unix域字节流协议等)。
参数说明:
domain参数:指明协议族,即你想要使用什么协议(IPV4、IPV6...),它是下列表格中的某个常值。该参数也往往被称为协议域。
domain说 明
AF_INET
IPV4协议
AF_INET6
IPV6协议
AF_LOCAL
Unixt域协议
AF_ROUTE
路由套接字
AF_KEY
密钥套接字
规定:我们接下来所使用的套接字所采用的协议都是AF_INET(IPV4协议)
type参数:指明套接字的类型,它是下列表格中的某个常值。
type说 明
SOCK_STREAM
字节流套接字
SOCK_DGRAM
数据报套接字
SOCK_SEQPACKET
有序分组套接字
SOCK_RAW
原始套接字
如果你是要TCP通信的话,就要是要SOCK_STREAM作为类型,UDP就使用SOCK_DGRAM作为类型。
protocol参数:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。
protocol说 明
IPPROTO_TCP
TCP传输协议
IPPROTO_UDP
UDP传输协议
IPPROTO_SCTP
SCTP传输协议
返回值说明:
套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置。
2. 绑定端口号
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind函数是把一个协议地址赋予一个套接字。
参数说明:
sockfd参数:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
addr参数:这个参数是指向一个特定于协议的地址结构的指针。里面包含了协议族、端口号、IP地址等。(见下一节sockaddr结构中的介绍)
addrlen参数:是该协议的地址结构的长度。
返回值说明:
绑定成功返回0,绑定失败返回-1,同时错误码会被设置。
3. 监听套接字
// 开始监听socket (TCP, 服务器)
int listen(int sockfd, int backlog);
listen函数仅由TCP服务器调用,表明服务器对外宣告它愿意接受连接请求,它做两件事:
1.当socket函数创建一个套接字时,它被假设为一个主动套接字,也就是说将调用connect发起连接的客户端套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。简单来说,服务器调用listen函数,就是告诉客户端你可以连接我了。
2.第二个参数规定了内核应该为相应的套接字排队的最大连接个数。backlog提供了一个提示,提示系统该进程要入队的未完成连接的请求数量。其实际由系统决定,对于TCP而言,默认是128。
一旦队列满了,系统就会拒绝多余的连接请求,所有backlog的值应该基于服务器期望负载和处理量来选择,其中处理量是指接受连接请求与启动服务的数量。
一旦服务器调用了listen,所用的套接字就能接受连接请求。使用accept函数获得的连接请求并建立连接。
返回值:成功返回0,失败返回-1;
本函数通常应该在调用socket和bind这两个函数之后,并在调用accept函数之前调用。
4. 接受请求
// 接收请求 (TCP, 服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept函数是由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程将被投入睡眠。
如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。我们常常称它的第一个参数为监听套接字(listeningsocket) 描述符(由socket创建,随后用作bind和listen的第一个参数的描述符)unix进程通信,称它的返回值为已连接套接字(connectedsocket)描述符。区分这两个套接字非常重要。一个服务器通常仅仅创建一个监听套接字,它在该服务器的生命期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接字(也就是说对于它的TCP三路握手过程已经完成)。当服务器完成对某个给定客户的服务时,相应的已连接套接字就被关闭。
总的来说,函数accept所返回的文件描述符是新的套接字描述符,该描述符连接到调用connect的客户端。这个新的套接字描述符和原始套接字(sockfd)具有相同的套接字类型和地址族。传给accept的原始套接字没有关联到这个连接,而是继续保持监听状态并接受其他连接请求。
5. 建立连接
// 建立连接 (TCP, 客户端)
#included
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
TCP客户用connect函数来建立与TCP服务器的连接。
参数说明:
sockfd参数:是由socket函数返回的套接字描述符,第二个以及第三个参数分别是指向套接字地址结构的指针和该结构的大小。
在connect中指定的地址是我们想要与之通信服务器地址。如果sockfd没有绑定到一个地址,connect会给调用者绑定一个默认地址。
返回值说明:
若成功则为0,若出错则为-1;
2. 套接字地址结构(sockaddr)
从介绍的套接字函数接口来看,bind函数、accept函数和conect函数都有一个struct sockaddr的结构体指针,我们在介绍参数的时候也已经说了,这种结构是指向一个特定于协议的地址结构的指针。里面包含了协议族、端口号、IP地址等。
在网络通信的时候,其标准方式有多种,比如:IPV4套接字地址结构——struct sockaddr_in,Unix域套接字地址结构——struct sockaddr_un;前者属于网络通信,后者属于域间通信;
也就是说我们的套接字接口就这么一套,但是通信方式确有多种,你只需要给这个结构体(struct sockaddr)传输你想要的通信方式即可;其实也不难看出,这种就类似于多态,所有的通信方式都是子类,struct sockaddr就是父类,父类指向不同的子类,就使用不同的方法;
我们要做的就是在使用的时进行强制类型转换即可;可能你会想到在C语言中有一个指针void*,它的功能就是可以接受任意类型的指针,再进行强转也可以。但是,早期在设计的时候还没有void*这种指针,所以这种用法一直延续至今。

大多数套接字函数都需要指向套接字地址结构的指针作为参数。每个协议族都定义了它自己的套接字地址结构。这些地址结构的名字均已sockaddr_开头。

3. struct sockaddr 、struct sockaddr_in 、struct sockaddr_un的区别及转换
由于通信方式的种类有多种,套接字接口只有一套,如果给每个通信方式都设计一套接口,单从技术的角度来说,完全是可以的。但是从使用者和学习者来讲,无疑是增加了负担。所以早期在设计的时候,就单独设计了一个通用的套接字地址结构,我们只要给这个通用的套接字地址结构传入不同的套接字地址结构,然后进行强转。在地址结构中给到我们想要通信的IP地址、端口号以及所采用的协议族。
/************************通用套接字地址结构***********************************/
/* /usr/include/bits/socket.h */ /*原码所在目录,通过vim命令查看*/
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
/*************************IPV4套接字地址结构**********************************/
/* /usr/include/netinet/in.h */ /*原码所在目录,通过vim命令查看*/
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
/*************************Unix域套接字地址结构***********************************/
/* /usr/include/sys/un.h */ /*原码所在目录,通过vim命令查看*/
/* Structure describing the address of an AF_LOCAL (aka AF_UNIX) socket. */
struct sockaddr_un
{
__SOCKADDR_COMMON (sun_);
char sun_path[108]; /* Path name. */
};
其中3个结构里都包含了 __SOCKADDR_COMMON 这个宏,我们先把它的定义找到;
/* /usr/include/bits/sockaddr.h*/ /*原码所在目录,通过vim命令查看*/
/* POSIX.1g specifies this type name for the `sa_family' member. */
typedef unsigned short int sa_family_t;
/* This macro is used to declare the initial common members
of the data types used for socket addresses, `struct sockaddr',
`struct sockaddr_in', `struct sockaddr_un', etc. */
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
这三个结构的第一个字段都是一个unsigned short int 类型,只不过用宏来定义了三个不同的名字,至此第一个结构就清楚了,在一般环境下(short一般为2个字节),整个结构占用16个字节,变量sa_family占用2个字节,变量sa_data 保留14个字节用于保存IP地址信息。
接着我们发现第二个结构中还有in_port_t和struct in_addr两个类型没有定义,继续找下去吧:
/* /usr/include/bits/socket.h */ /*原码所在目录,通过vim命令查看*/
/* Type to represent a port. */
typedef uint16_t in_port_t;
/* Internet address. */
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;
};
这么看来sockaddr_in这个结构也不复杂,除了一开始的2个字节表示sin_family,然后是2个字节的变量sin_port表示端口,接着是4个字节的变量sin_addr表示IP地址,最后是8个字节变量sin_zero填充尾部,用来与结构sockaddr对齐。
那接下来我们整理一下,为了看的清楚,部分结构使用伪代码,不能通过编译,主要是方便理解:
/*通用套接字地址结构*/
struct sockaddr
{
uint16 sa_family; /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
/*IPV4套接字地址结构*/
struct sockaddr_in
{
uint16 sin_family; /* Address family AF_INET */
uint16 sin_port; /* Port number. */
uint32 sin_addr.s_addr; /* Internet address. */
unsigned char sin_zero[8]; /* Pad to size of `struct sockaddr'. */
};
/*Unix域套接字地址结构*/
struct sockaddr_un
{
uint16 sin_family; /* Address family AF_LOCAL */
char sun_path[108]; /* Path name. */
};
附图:源码结构

三、简单的TCP网络程序
基本TCP客户/服务器通信流程如下,主要为了大家能够更好的理解

1. TCP客户端代码
配有代码详解图