0%

套接字

套接字的使用,以及使用过程中的注意事项和相关知识

套接字socket

socket套接字本来是用于同主机的进程间通信的。后来有了TCP/IP协议族加入后又进行的功能的拓展与升级,实现了多个不同主机间的进程间通信。

Linux内核的五大功能:

  1. 进程间管理:进程间调度及上下文切换
  2. 内存管理:内存的分配与回收。
  3. 文件管理:将二进制数据进行长时间的保存及管理。
  4. 设备管理:一切皆文件。
  5. 网络管理:网络协议栈的管理。

socket是一个系统调用的接口,会返回一个文件描述符,用户通过网络收发数据时,只需对此文件描述符进行读写操作即可,读就是接收,写就是发送。相当于把复杂的网络通信过程转换成了IO操作。

也可以将Socket(套接字)看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。

套接字Socket =(IP地址:端口号),套接字的表示方法是点分十进制的lP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。
例如:如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23)

套接字类型

**流套接字(SOCK_STREAM)**:流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即TCP(The Transmission Control Protocol)协议

**数据报套接字(SOCK_DGRAM)**:数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理

**原始套接字(SOCK_RAW)**:原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据报套接字)的区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据报套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接

字节序

字节序是计算机存储多字节数据的方式,目前主流的方式有:大端字节序和小端字节序,字节序主要是针对多字节的数据类型,比如 short、int 等类型

  • 小端字节序
    数据高位字节存储在内存的高地址上,数据低位字节存储在内存的低地址上
  • 大端字节序
    数据高位字节存储在内存的低地址上,数据低位字节存储在内存的高位地址上
    Alt text

因为大端对人们阅读更友好,所以 IEEE 标准协会规定除非有明确说明,否则网络协议都使用大端字节序, 像 TCP/IP 就使用大端序。而计算机的存储则以小端序为主。

判断大小端

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(int argc, char const *argv[])
{
unsigned int numb = 0x12345678;
char* p = (char*)&numb;
printf("%x \n",p[0]);
return 0;
}

大小端转换

1
2
3
4
5
6
7
8
9
10
11
//如果是Linux:
//1969年留下来的头文件。
#include <arpa/inet.h>
//如果是Windows:
#include <winsocket.h>

ntohl(u_long netOrder) # uint32 类型 网络序转主机序
htonl(u_short hostOrder) # uint32 类型 主机序转网络序

ntohs(u_long netOrder) # uint16 类型 网络序转主机序
htons(u_short hostOrder) # uint16 类型 主机序转网络序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char const *argv[])
{
unsigned short numb = 0x1234;
char* p = (char*)&numb;
printf("%x \n",p[0]);
//主机字节序转网络字节络:
unsigned short numb_big = htons(numb);
p = (char*)&numb_big;
printf("%x \n",p[0]);
//网络字节序转主机字节序:
unsigned short numb_small = ntohs(numb_big);
p = (char*)&numb_small;
printf("%x \n",p[0]);
return 0;
}

IP地址转换

1
2
3
4
// 将点分十进制字符串转换成网络字节序的无符号四字节整形的转换函数:
in_addr_t inet_addr(const char* cp);自动转成网络字节序。
// 将网络字节序的无符号四字节整型的ip 地址转成点分十进制的字符串:
char* inet_ntoa(struct in_addr in);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
int main(int argc, char const *argv[])
{
const char* IP = "192.168.0.1";
//inet_addr把点分十进制字符串转换成网络字节序的无符号4字节整形。
in_addr_t netOrderIp = inet_addr(IP);
printf("%d \n",netOrderIp);
unsigned char* p = (unsigned char*)&netOrderIp;
printf("%d.%d.%d.%d \n",p[0],p[1],p[2],p[3]);
//通过inet_htoa(in_addr )把网络字节序的无符号整形转换成char*的字符串。
struct in_addr ip = {0};
ip.s_addr = netOrderIp;
printf("------------------------------\n");
char* strip = inet_ntoa(ip);
printf("%s \n",strip);
return 0;
}

端口号

网络通信中的两个进程间互联时的一种约定好的进程的标识

端口号使用规则 :TCPUDP段结构中端口地址都是16比特,可以有在0—65535范围内的端口号。对于这65536个端口号有以下的使用规定

  1. 端口号小于256的定义为常用端口,服务器一般都是通过常用端口号来识别的。任何TCP/IP实现所提供的服务都用1—1023之间的端口号,是由ICANN来管理的;端口号从1024—49151是可被注册的端口,也成为“用户端口”,49152-65535被IANA指定为特殊服务使用。在Linux中可以通过/etc/services下可查看已经占用的端口号。
  2. 客户端只需保证该端口号在本机上是唯一的就可以了。客户端端口号因存在时间很短暂又称临时端口号。
  3. 大多数TCP/IP实现给临时端口号分配1024—5000之间的端口号。大于5000的端口号是为其他服务器预留的。

端口号是2^16次方,所以是超过了一个字节,是两个字节,所以在指定端口时,也需要注意向网络字节序进行转换的问题。
所以字节或字节数组在网络传输不需要关系字节序的问题,但是对于多字节的数据类型一定要注意字节序的问题。

Client/Server(C/S)

Alt text

  1. 创建套接字
  2. 套接字绑定
  3. 监听套接字
  4. 建立链接请求
  5. 读写
  6. 关闭套接字

创建套接字 - socket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <sys/types.h>        
#include <sys/socket.h>
/*
* @功能:创建套接字,并返回一个套接字文件描述符。
* @参数:
* @domain:domain参数指定通信域;这将选择用于通信的协议族。
* 这些系列被定义在<sys/socket.h>中。
* 目前被Linux内核理解的格式包括:
* Name Purpose Man page
* AF_UNIX, AF_LOCAL Local communication unix(7)
* AF_INET IPv4 Internet protocols ip(7)
* AF_INET6 IPv6 Internet protocols ipv6(7)
* AF_IPX IPX - Novell protocols
* AF_NETLINK Kernel user interface device netlink(7)
* AF_X25 ITU-T X.25 / ISO-8208 protocol x25(7)
* AF_AX25 Amateur radio AX.25 protocol
* AF_ATMPVC Access to raw ATM PVCs
* AF_APPLETALK AppleTalk ddp(7)
* AF_PACKET Low level packet interface packet(7)
* AF_ALG Interface to kernel crypto API
* @type:套接字具有指定的类型,该类型指定通信语义。目前定义的类型有:
* SOCK_STREAM ---->流式套接字
*
* SOCK_DGRAM ---->数据报套接字
*
* SOCK_RAW ---->原始套接字
* @protocol:协议指定套接字使用的特定协议。在给定的协议族中,
* 通常只存在“一个协议”来支持特定的套接字类型,在这种情况下,“协议默认为0”。
* @返回值:成功返回套接字文件描述符,失败返回-1。
*/
int socket(int domain, int type,int protocol);

内核中的事情:
当进程调用socket()函数创建一个套接字时,操作系统会在内核中分配一些资源来管理这个套接字。具体来说,内核会创建一个数据结构来表示该套接字,该数据结构存储了套接字的各种属性,如协议类型、地址信息、发送和接收缓冲区等。此外,内核还会分配一些缓存区来存储套接字的数据,例如接收到的数据和将要发送的数据。

当进程调用bind()函数绑定套接字地址时,内核会将指定的套接字地址绑定到套接字数据结构中,以便进程可以通过该套接字地址来发送和接收数据。如果进程调用listen()函数将套接字转换为被动套接字,内核还会为套接字创建一个等待连接的队列。

总之,socket()函数会在内核中开辟一些资源来管理套接字,包括套接字的数据结构和缓存区等。这些资源会随着套接字的使用而不断变化,内核会负责管理这些资源,以保证进程可以正常地发送和接收数据。

套接字绑定 - bind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
/*
* @功能:给套接字绑定一个ip地址及端口,底层为套接字标识的区域分配空间。
* @参数:
* @sockfd:套接字文件描述符。
* @addr:指向要绑定给套接字的地址信息的指针。
* @addrlen:地址信息的长度。
* @返回值:成功返回0,失败返回-1。
*/
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//给套接字绑定一个ip地址及端口,底层为套接字标识的区域分配空间。
// sockaddr 为一个通用结构体,主要用的转型使用。
//因为以后可能会选择不同的通信方式比如ipv6或其它等。不同的通信方式结构体都不一样。

// IPv4套接字信息结构体:
struct sockaddr_in
{
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr
{
uint32_t s_addr; /* address in network byte order */
};

// IPv6套接字信息结构体:
struct sockaddr_in6 {
sa_family_t sin6_family; // address family (AF_INET6)
in_port_t sin6_port; // port number
struct in6_addr sin6_addr; // IPv6 address
uint32_t sin6_flowinfo; // IPv6 flow information
uint32_t sin6_scope_id; // IPv6 scope ID
};
/* Internet address. */
struct in6_addr {
union {
uint8_t __u6_addr8[16];
uint16_t __u6_addr16[8];
uint32_t __u6_addr32[4];
} __u6_addr; // 128-bit IPv6 address
};

// 在使用bind时,使用通用套接字sockaddr类型指针来进行,
// 因为bind函数只用到协议标识,端口与地址。所以需要进行一下指针类型转型

内核中的事情:

  1. 内核会检查传递的网络信息结构是否合法。如果结构体不合法,bind()函数会失败并返回一个错误。
  2. 如果结构体合法,则内核会将套接字与指定的网络地址和端口号绑定。这将为套接字分配一个本地 IP 地址和端口号,并将该地址添加到套接字的控制块中。
  3. 如果指定的端口号为0,则内核会为套接字分配一个可用的随机的端口号。
  4. 如果套接字绑定的是 INADDR_ANY(通配地址),则内核会将套接字绑定到所有可用的网络接口,并为套接字分配一个本地 IP 地址。
  5. 内核会根据套接字的类型和协议确定套接字的传输方式,并分配一些内部数据结构和缓冲区来管理套接字。

总之,通过将套接字的网络信息结构传递给bind()函数,内核会将套接字与指定的网络地址和端口号绑定,并执行一系列操作来为套接字分配地址、管理套接字并提供网络通信服务。

监听套接字 - listen

1
2
3
4
5
6
7
8
9
10
11
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

/*
* @功能:将套接字转换为被动套接字,用于监听客户端的连接请求。
* @参数:
* @sockfd:套接字文件描述符。
* @backlog:指定内核为相应套接字排队的最大连接个数。
* @返回值:成功返回0,失败返回-1。
*/
int listen(int sockfd, int backlog);

内核中的事情:

  1. 内核会将该套接字标记为被动套接字(passive socket),即告诉内核该套接字将被用来接受客户端连接请求。
  2. 内核会创建一个等待连接的队列,并将该套接字加入到该队列中。客户端连接请求首先会被放入等待连接的队列中,然后再由进程调用accept()函数来处理它们。
  3. 内核会开始监听该套接字,以便接受来自客户端的连接请求。这意味着内核会开始接受传入的连接请求,但并不会自动建立连接,而是将连接请求放入等待连接的队列中。
  4. listen()函数会返回一个成功的状态,表示该套接字已经被设置为监听状态。

总之,listen()函数背后内核会执行一系列的操作来将一个套接字设置为监听状态,包括创建等待连接的队列、加入套接字到等待连接队列、开始监听套接字等。通过这些操作,内核为进程提供了一种简单、可靠的方式来接受来自客户端的连接请求。

建立链接请求 - accept

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

/*
* @功能:接受客户端的连接请求,并返回一个新的套接字文件描述符。
* @参数:
* @sockfd:套接字文件描述符。
* @addr:指向要绑定给套接字的地址信息的指针。
* @addrlen:地址信息的长度。
* @返回值:成功返回新的套接字文件描述符,失败返回-1。
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

内核中的事情:

  1. 当调用accept()函数时,内核会检查该套接字是否处于监听状态。如果不是,会阻塞等待直到该套接字处于监听状态。
  2. 内核会检查等待连接的队列中是否有客户端连接请求,如果有,则内核会创建一个新的套接字来处理该客户端连接,并从等待连接的队列中删除该连接请求。如果没有,则accept()函数会阻塞等待,直到有连接请求到达为止。
  3. 内核会为新创建的套接字分配一个新的文件描述符,并把它返回给进程。这个新的文件描述符和原来的套接字文件描述符是不同的。
  4. 接下来,内核会为新创建的套接字建立一个连接,并返回一个连接成功的状态。
  5. 如果连接建立成功,则进程就可以使用新的套接字来与客户端进行通信了。如果连接建立失败,则会返回一个错误码,进程需要根据错误码进行处理。

总之,accept()函数背后内核会执行一系列的操作来处理客户端连接请求,包括检查监听状态、等待连接请求、创建新的套接字、分配新的文件描述符、建立连接等。通过这些操作,内核为进程提供了一种简单、可靠的方式来处理客户端连接请求。

通信的本质 - read()/write() — 数据收发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>

/*
* @功能:从套接字中读取数据。
* @参数:
* @sockfd:套接字文件描述符。
* @buf:指向接收数据的缓冲区。
* @count:要读取的字节数。
* @返回值:成功返回读取的字节数,失败返回-1。
*/
ssize_t read(int fd, void *buf, size_t count);


#include <unistd.h>

/*
* @功能:向套接字中写入数据。
* @参数:
* @sockfd:套接字文件描述符。
* @buf:指向发送数据的缓冲区。
* @count:要写入的字节数。
* @返回值:成功返回写入的字节数,失败返回-1。
*/
ssize_t write(int fd, const void *buf,size_t count);

内核中的事情:

在调用read()函数读取套接字时

  1. 内核会检查套接字的接收缓冲区是否有数据可读。如果接收缓冲区为空,则read()函数会阻塞等待,直到有数据可读。
  2. 如果接收缓冲区有数据可读,则内核会将数据从接收缓冲区复制到进程的缓冲区中。
  3. 内核会检查进程的缓冲区是否有足够的空间来存储读取的数据。如果没有足够的空间,则read()函数会阻塞等待,直到有足够的空间为止。
  4. 如果有足够的空间,则内核会将读取的数据从内核空间复制到进程的缓冲区中,并更新套接字的接收缓冲区的读指针。读指针指向下一个要读取的位置。
  5. read()函数会返回读取的字节数,如果读取到套接字关闭,则返回0。

总之,调用read()函数读取套接字时,内核会执行一系列的操作来读取套接字中的数据,包括检查接收缓冲区、阻塞等待、读取数据、检查缓冲区空间、复制数据到进程空间等。通过这些操作,内核为进程提供了一种简单、可靠的方式来读取套接字中的数据。
在调用write()函数写入套接字时

  1. 内核会将写入的数据从进程的缓冲区复制到套接字的发送缓冲区中。
  2. 内核会检查套接字的发送缓冲区是否有足够的空间来存储写入的数据。如果发送缓冲区已满,则write()函数会阻塞等待,直到有足够的空间为止。
  3. 如果发送缓冲区有足够的空间,则内核会将数据从进程空间复制到套接字的发送缓冲区中,并更新发送缓冲区的写指针。写指针指向下一个要写入的位置。
  4. write()函数会返回写入的字节数。

总之,调用write()函数写入套接字时,内核会执行一系列的操作来写入套接字中的数据,包括复制数据到发送缓冲区、检查缓冲区空间、更新写指针等。通过这些操作,内核为进程提供了一种简单、可靠的方式来写入套接字中的数据。

关闭套接字 - close

1
2
3
4
5
6
7
8
9
#include <unistd.h>

/*
* @功能:关闭套接字。
* @参数:
* @sockfd:套接字文件描述符。
* @返回值:成功返回0,失败返回-1。
*/
int close(int sockfd);

内核中的事情:
当进程调用close()函数关闭套接字时,内核会释放该套接字占用的所有资源,包括套接字的数据结构、缓存区、等待连接的队列等。此时,进程不再能够使用这个套接字。

另外,如果进程崩溃或者异常终止,内核会自动回收该进程占用的所有资源,包括该进程创建的所有套接字。这个过程称为垃圾回收(Garbage Collection),由操作系统内核自动完成。

注意:如果有其他进程仍在使用某个套接字,那么该套接字的资源就不会被释放,直到所有使用该套接字的进程都调用了close()函数关闭它。这是因为操作系统内核负责管理套接字资源,并且要确保多个进程可以共享同一个套接字。只有当所有使用该套接字的进程都退出或关闭它时,内核才会回收该套接字的所有资源。