0%

网络相关知识:Socket简述

TCP/IP & UDP

TCP/IP(Transmission Control Protocol/Internet Protocol):

  • 输控制协议/网间协议,是一个工业标准的协议集(一系列网络协议的总和),它是为广域网设计的

UDP(User Data Protocol):

  • 用户数据报协议,是与 TCP 相对应的协议,它是属于 TCP/IP 协议族中的一种

Socket简述

Socket 就是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口:

  • socket 可以大大简化“网络通信编程”,我们不需要完全掌握这种编程的各个细节,只需要使用 socket 的接口就可以了

网络通信

本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:

  • 消息传递(管道、FIFO、消息队列)
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存(匿名的和具名的)
  • 远程过程调用(Solaris 门和 Sun RPC)

在进行网络通信之前,系统需要先“识别”程序,TCP/IP 协议族完成了这个功能:

  • 网络层的 ip地址 可以唯一标识网络中的主机
  • 传输层的 协议+端口 可以唯一标识主机中的应用程序(进程)

这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了

Socket函数

1
int socket(int domain, int type, int protocol)

用于创建一个 socket 描述符,它唯一标识一个 socket

  • domain:即协议域,又称为协议族(family),协议族决定了 socket 的地址类型,在通信中必须采用对应的地址
  • type:指定socket类型
  • protocol:故名思意,就是指定协议
  • return:返回一个文件描述符 sockfd(描述字它存在于协议族空间中,但没有一个具体的地址)
    • 如果想要给它赋值一个地址,就必须调用 bind() 函数
    • 否则就当调用 connect() 时系统会自动随机分配一个端口
1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

把一个地址族中的特定地址赋给 socket

  • sockfd:即socket描述字,它是通过 socket() 函数创建了,唯一标识一个socket,bind()函数就是将给这个描述字绑定一个名字
  • addr:一个 const struct sockaddr * 指针,指向要绑定给 sockfd 的协议地址,这个地址结构根据地址创建 socket 时的地址协议族的不同而不同
  • addrlen:地址的长度

通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器

而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合就行

  • 这就是为什么通常服务器端在 listen 之前会调用 bind,而客户端就不会调用,而是在 connect 时由系统随机生成一个

socket 函数创建的 socket 默认是一个主动类型的,listen 函数将 socket 变为被动类型的,等待客户的连接请求:

1
int listen(int sockfd, int backlog)
  • sockfd:即 socket 描述字
  • backlog:相应 socket 可以排队的最大连接个数

服务端通过调用 accept 函数来接受客户端的 connect 请求:

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
  • sockfd:即 socket 描述字
  • addr:服务器的 socket 地址
  • addrlen:地址的长度

客户端通过调用 connect 函数来建立与 TCP 服务器的连接(服务器必须先调用 listen 开启监听):

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
  • sockfd:即 socket 描述字
  • addr:服务器的 socket 地址
  • addrlen:地址的长度

TCP 服务器端依次调用 socket、bind、listen 之后,就会监听指定的 socket 地址了

TCP 客户端依次调用 socket、connect 之后就想 TCP 服务器发送了一个连接请求

TCP 服务器监听到这个请求之后,就会调用 accept 函数取接收请求,这样连接就建立好了

  • 如果 accpet 成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的 TCP 连接

三次握手 四次释放

一,TCP建立连接要进行“三次握手”,即交换三个分组,大致流程如下:

  • 客户端向服务器发送一个 SYN J
  • 服务器向客户端响应一个 SYN K,并对 SYN J 进行确认 ACK J+1
  • 客户端再想服务器发一个确认 ACK K+1
  • 当客户端调用 connect 时,触发了连接请求,向服务器发送了 SYN J 包,这时 connect 进入阻塞状态
  • 服务器监听到连接请求,即收到 SYN J 包,调用 accept 函数接收请求向客户端发送 SYN K ,ACK J+1,这时 accept 进入阻塞状态
  • 客户端收到服务器的 SYN K ,ACK J+1 之后,这时 connect 返回,并对 SYN K 进行确认,服务器收到 ACK K+1 时,accept 返回,至此三次握手完毕,连接建立

二,socket中有四次握手释放连接的过程,流程如下:

  • 某个应用进程首先调用 close 主动关闭连接,这时 TCP 发送一个 FIN M
  • 另一端接收到 FIN M 之后,执行被动关闭,对这个 FIN 进行确认(它的接收也作为文件结束符 EOF 传递给应用进程,因为 FIN 的接收意味着应用进程在相应的连接上再也接收不到额外数据)
  • 一段时间之后,接收到文件结束符 EOF 的应用进程调用 close 关闭它的 socket,这导致它的 TCP 也发送一个 FIN N
  • 接收到这个 FIN 的源发送端 TCP 对它进行确认
  • 这样每个方向上都有一个 FIN 和 ACK

本地进程间通信案例

服务端

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <stdio.h>  
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#define CAN_SERVICE "B"
int main(void)
{
int ret;
int len;
int accept_fd;
int socket_fd;
static char recv_buf[1024];
socklen_t clt_addr_len;
struct sockaddr_un clt_addr;
struct sockaddr_un srv_addr;

socket_fd=socket(PF_UNIX,SOCK_STREAM,0);
if(socket_fd<0)
{
perror("cannot create communication socket");
return 1;
}

// 设置服务端参数(服务端的sockaddr必须和客户端的sockaddr一样)
srv_addr.sun_family=AF_UNIX;
strncpy(srv_addr.sun_path,CAN_SERVICE,sizeof(srv_addr.sun_path)-1);
unlink(CAN_SERVICE);

// 绑定socket地址
ret=bind(socket_fd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));
if(ret==-1)
{
perror("cannot bind server socket");
close(socket_fd);
unlink(CAN_SERVICE);
return 1;
}

// 监听
ret=listen(socket_fd,1);
if(ret==-1)
{
perror("cannot listen the client connect request");
close(socket_fd);
unlink(CAN_SERVICE);
return 1;
}

// 接受connect请求
len=sizeof(clt_addr);
accept_fd=accept(socket_fd,(struct sockaddr*)&clt_addr,&len);
if(accept_fd<0)
{
perror("cannot accept client connect request");
close(socket_fd);
unlink(CAN_SERVICE);
return 1;
}

// 读取和写入
memset(recv_buf,0,1024);
int num=read(accept_fd,recv_buf,sizeof(recv_buf));
printf("Message from client (%d)) :%s\n",num,recv_buf);

// 关闭socket
close(accept_fd);
close(socket_fd);
unlink(CAN_SERVICE);
return 0;
}

客户端

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
#include <stdio.h>  
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#define CAN_SERVICE "B"
int main(void)
{
int ret;
int socket_fd;
char snd_buf[1024];
static struct sockaddr_un srv_addr;

printf("%d\n",PF_UNIX);
printf("%d\n",SOCK_STREAM);

// 创建socket
socket_fd=socket(PF_UNIX,SOCK_STREAM,0);
if(socket_fd<0)
{
perror("cannot create communication socket");
return 1;
}

// 设置客户端参数(客户端的sockaddr必须和服务端的sockaddr一样)
srv_addr.sun_family=AF_UNIX;
strcpy(srv_addr.sun_path,CAN_SERVICE);

// 连接到服务端
ret=connect(socket_fd,(struct sockaddr*)&srv_addr,sizeof(srv_addr));
if(ret==-1)
{
perror("cannot connect to the server");
close(socket_fd);
return 1;
}
memset(snd_buf,0,1024);
strcpy(snd_buf,"message from client");

// 读取和写入
write(socket_fd,snd_buf,sizeof(snd_buf));
close(socket_fd);
return 0;
}
  • 当 bind 执行以后,当前目录会出现一个名为 “B”(CAN_SERVICE) 的文件
  • 当 unlink(CAN_SERVICE) 执行以后,文件 “B” 消失
  • 如果不及时调用 unlink 的话,会出现 Address already in use 报错

参考:超详细的Socket通信原理和实例讲解 - 知乎