socket编程
1.什么是socket?
- socket可以看作是用户进程与内核网络协议栈的编程接口
- socket不仅可以用于本机上的进程间通信(IPC),还可以用于网路上不同主机的进程间通信(IPC)。
- 同时socket网络还可以用于异构系统间进行通信:如主机A上 (使用的是手机,常见架构为 ARM, 操作系统如安卓,苹果,鸿蒙等)的QQ要与B主机(是一台PC,常见架构为x86,操作系统如:Windows, Linux等)
2.
2.1 IPv4套接字地址结构
IPv4 地址被分为单播、广播和多播地址。单播地址指定一个主机的单个接口,广播地址指定网络上的所有主机,多播地址则针对一个多播组中的所有主机。只有在设置了 SO_BROADCAST 套接字标志时,才能发送或接收发往广播地址的数据报。在当前的实现中,面向连接的套接字仅允许使用单播地址。
IPv4套接字地址结构通常也称为“网际套接字地址结构”,以"scokaddr_in"进行命名
在Linux内核中,/include/uapi/linux./in.h中的源码为
/* Structure describing an Internet (IP) socket address. */
#if __UAPI_DEF_SOCKADDR_IN
#define __SOCK_SIZE__ 16 /* sizeof(struct sockaddr) */
struct sockaddr_in {__kernel_sa_family_t sin_family; /* Address family */__be16 sin_port; /* Port number */struct in_addr sin_addr; /* Internet address *//* Pad to size of `struct sockaddr'. */unsigned char __pad[__SOCK_SIZE__ - sizeof(short int) -sizeof(unsigned short int) - sizeof(struct in_addr)];
};
#define sin_zero __pad /* for BSD UNIX comp. -FvK */
#endif
其中重要字段为 sin_family 、sin_port和sin_addr 通过以下 bash 指令也可查看
man 7 ip
sin_family 始终设置为 AF_INET。这是必需的;在 Linux 2.2 中,大多数网络函数在缺少此设置时会返回 EINVAL。sin_port 包含网络字节序中的端口。低于 1024 的端口号称为特权端口(有时称为:保留端口)。
只有特权进程(在 Linux 中:具有 CAP_NET_BIND_SERVICE 能力的进程,其在管理其网络命名空间的用户命名空间中)才可以绑定到这些套接字。请注意,原始 IPv4 协议本身没有端口的概念,它们仅由更高层的协议如 tcp(7) 和 udp(7) 实现。
sin_addr 是 IP 主机地址。struct in_addr 的 s_addr 成员包含以网络字节顺序表示的主机接口地址。in_addr 应该被赋值为 INADDR_* 之一(例如,INADDR_LOOPBACK),可以使用 htonl(3) 或通过 inet_aton(3)、inet_addr(3)、inet_makeaddr(3) 库函数设置,也可以直接使用名称解析器。
请注意,地址和端口始终以网络字节顺序存储。特别是,这意味着您需要在分配给端口的数字上调用 htons(3)。标准库中的所有地址/端口操作函数都以网络字节顺序工作。
有几个特殊地址:
1) INADDR_LOOPBACK(127.0.0.1)始终通过回环设备指向本地主机;
2) INADDR_ANY(0.0.0.0)表示用于绑定的任何地址;
3) INADDR_BROADCAST(255.255.255.255)表示任何主机,并且由于历史原因,其在绑定上的效果与 INADDR_ANY 相同。
- 字节序:
- 大端字节序:
最高有效位(MSB:Most Significant Bit)存储在最低内存地址,最低有效位(LSB:Lowest Significant Bit)存储在最高内存地址 - 小端字节序:
最高有效位(MSB:Most Significant Bit)存储在最高内存地址,最低有效位(LSB:Lowest Significant Bit)存储在最低内存地址(符合阅读习惯)
- 大端字节序:
- 主机字节序
不同的主机由不同的字节序,如x86位小端字节序,ARM字节序可配置 - 网络字节序
上文中提到的网络字节序,规定为大端字节序
Ubuntu下的字节序(大端)
Linux字节转换函数,网络号转换函数
2.2 套接字类型
- 流式套接字(SOCK_STREAM):TCP协议
提供面向连接的、可靠的数据传输服务,数据无差错,无重复的发送,且按发送顺序接收 - 数据包式套接字(SOCK_DGRAM):UDP
提供无连接服务。不提供无错保证,数据可能丢失或重复,并且接收顺序混乱 - 原始套接字(SOCK_RAW)
3.socket编程实战
3.1 TCP客户/服务器模型
3.1.1 原理图
3.1.2过程描述
1.服务器端
-
socket()
服务端首先调用socket()
函数,创建一个用于通信的套接字(socket)。 -
bind()
服务器将创建的socket与本地的IP地址和端口号绑定,指定服务器监听的地址和端口。 -
listen()
服务器调用listen()
函数,将socket设置为监听状态,准备接受客户端的连接请求。 -
accept()
服务器调用accept()
函数,阻塞等待客户端的连接请求到来。连接到达后,accept()
返回一个新的socket用于与客户端通信。 -
read()
服务器从客户端读取数据,通常是客户端的请求内容。 -
处理请求
服务器根据客户端请求进行相应处理。 -
write()
服务器向客户端发送处理后的数据(应答)。 -
read()
继续等待客户端发送数据或结束通知。 -
close()
关闭与客户端的连接,释放资源。
2.客户端
-
socket()
客户端首先调用socket()
函数,创建自己的套接字。 -
connect()
客户端调用connect()
函数,向服务端指定的IP和端口发起连接请求。此时会进行TCP三次握手,建立连接。 -
write()
客户端向服务端发送数据(通常是请求信息)。 -
read()
客户端等待并读取服务端返回的数据(应答)。 -
close()
通信结束后,客户端关闭连接。
3.关键交互
-
建立连接(TCP三次握手)
客户端connect()
发起连接,服务器accept()
接受连接,请求被服务器监听并响应,双方通过三次握手建立可靠的连接。 -
数据传递
客户端通过write()
发送请求,服务器通过read()
读取请求并处理,再通过write()
将结果返回客户端,客户端用read()
接收。 -
连接关闭
通信结束后,客户端和服务端均可调用close()
关闭连接,通常由客户端主动发起。
3.1.3 socket函数
包含头文件<sys/socket.h>
- 功能:创建一个套接字用于通信
- 原型
-
glibc中对socket函数的声明如下
/* Create a new socket of type TYPE in domain DOMAIN, usingprotocol PROTOCOL. If PROTOCOL is zero, one is chosen automatically.Returns a file descriptor for the new socket, or -1 for errors. */ extern int socket (int __domain, int __type, int __protocol) __THROW;
-
- 参数
- int __domain: 指定通信协议族(protocol family)
- int __type: 指定socket类型,流式套接字SOCK_STREAM,数据包式套接字SOCK_DGRAM,原始套接字SOCK_RAW
- int __protocol: 协议类型
- 返回值:成功返回非负整数,与文件描述符类似,称为套接字描述符,简称套接字;失败返回-1
3.1.4 bind函数
- 包含头文件<sys/socket.h>
- 功能:绑定一个本地地址到套接字
- 原型
- glibc中对bind函数的声明如下
/* Give the socket FD the local address ADDR (which is LEN bytes long). */ extern int bind (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len)__THROW;
- glibc中对bind函数的声明如下
- 参数
- int __fd: 套接字文件描述符,指定socket
- __CONST_SOCKADDR_ARG __addr:指向本地地址结构体的指针,如struct sockaddr等
- socklen_t __len: addr所指向的地址结构体长度(字节数)
- 返回值:成功返回0,失败返回-1
3.1.5 listen函数
- 包含头文件<sys/socket.h>
- 功能:将套接字用于监听进入的连接
- 原型
-
/* Prepare to accept connections on socket FD.N connection requests will be queued before further requests are refused.Returns 0 on success, -1 for errors. */ extern int listen (int __fd, int __n) __THROW;
-
- 参数
- int __fd: 套接字文件描述符
- int __n: 规定内核为此套接字排队的最大连接个数
- 返回值:成功返回0,失败返回-1
- 一般来说,listen函数应该在调用 socket和bind函数之后,调用函数accept之前调用
- 对于给定的监听套接口,内核要维护两个队列:
- 1. 已经由客户发出并到达服务器,服务器正在等待完成相应的TCP三路握手过程
- 2. 已完成连接的队列
3.1.6 accept函数
- 包含头文件<sys/socket.h>
- 功能:从已完成连接队列返回第一个连接,如果已完成连接队列为空,则阻塞
- 原型
- glibc中函数原型
/* Await a connection on socket FD.When a connection arrives, open a new socket to communicate with it,set *ADDR (which is *ADDR_LEN bytes long) to the address of the connectingpeer and *ADDR_LEN to the address's actual length, and return thenew socket's descriptor, or -1 for errors.This function is a cancellation point and therefore not marked with__THROW. */ extern int accept (int __fd, __SOCKADDR_ARG __addr,socklen_t *__restrict __addr_len);
- glibc中函数原型
- 参数
- int __fd: 套接字文件描述符
- __SOCKADDR_ARG _addr: 指向本地地址结构体的指针,如struct sockaddr等
- socklen_t* __restrict __addr_len: 指向一个 socklen_t 类型的变量的指针——调用前,变量应设置为 __addr 指向的 sockaddr 结构体的大小;调用后,被内核修改为实际返回的地址长度。
- 返回值:成功返回非负整数,失败返回-1
3.1.7 connect函数
- 包含头文件<sys/socket.h>
- 功能:主动连接远端主机,对于面向连接的套接字(TCP),用于建立连接;对于无连接套接字(如UDP),则设置默认收发地址
- 原型(glibc中函数原型)
-
/* Open a connection on socket FD to peer at ADDR (which LEN bytes long).For connectionless socket types, just set the default address to send toand the only address from which to accept transmissions.Return 0 on success, -1 for errors.This function is a cancellation point and therefore not marked with__THROW. */ extern int connect (int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);
-
- 参数
- int __fd: 套接字文件描述符,由 socket() 创建。
- __CONST_SOCKADDR_ARG __addr: 指向 struct sockaddr 结构体的指针,表示要连接的远端主机地址信息(如 struct sockaddr_in)。
- socklen_t __len: __addr 所指结构体的长度(字节数),如 sizeof(struct sockaddr_in)。
- 返回值:成功返回非负整数,失败返回-1
在了解了以上函数的基础上就可以构建一个简易的 回射服务器/客户
以下是 client.c
#define ERR_EXIST(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \}while(0)int main(void)
{int sock;if((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0){ERR_EXIST("socket error");}struct sockaddr_in serve_addr;memset(&serve_addr, 0, sizeof(serve_addr));serve_addr.sin_family = AF_INET;serve_addr.sin_port = htons(5188);serve_addr.sin_addr.s_addr = inet_addr("127.0.0.1");if(connect(sock, (struct sockaddr*)&serve_addr, sizeof(serve_addr)) < 0){ERR_EXIST("connect error");}char send_buf[1024] = {0};char recv_buf[1024] = {0};while(fgets(send_buf, sizeof(send_buf), stdin) != NULL){write(sock, send_buf, strlen(send_buf));read(sock, recv_buf, strlen(recv_buf));fputs(recv_buf, stdout);memset(send_buf, 0, sizeof(send_buf));memset(recv_buf, 0, sizeof(send_buf));}close(sock);return 0;
}
以下是 server.c
#define ERR_EXIST(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \}while(0)int main(void)
{int listen_fd;if((listen_fd = socket(PF_INET, SOCK_STREAM, 0)) < 0)//创建失败ERR_EXIST("socket error");//bind();struct sockaddr_in serve_addr;memset(&serve_addr, 0, sizeof(serve_addr));serve_addr.sin_family = AF_INET;serve_addr.sin_port = htons(5188);//注意转换为网络字节序//1.serve_addr.sin_addr.s_addr = htonl(INADDR_ANY);//2.serve_addr.sin_addr.s_addr = inet_addr("127.0.0.1");//3.inet_aton("127.0.0.1", &serve_addr.sin_addr);if(bind(listen_fd, (struct sockaddr*)&serve_addr, sizeof(serve_addr)) < 0)ERR_EXIST("bind error");if(listen(listen_fd, SOMAXCONN) < 0)ERR_EXIST("listen error");struct sockaddr_in peer_addr;//必须有初始值socklen_t peer_len = sizeof(peer_addr);int conn;if((conn = accept(listen_fd, (struct sockaddr*)&peer_addr, &peer_len)) < 0)ERR_EXIST("accept error");char recvbuf[1024];while(1){memset(recvbuf, 0, sizeof(recvbuf));int ret = read(conn, recvbuf, sizeof(recvbuf));fputs(recvbuf, stdout);write(conn, recvbuf, ret);}close(conn);close(listen_fd);return 0;
}
在编译后,由于是两个主机之间的通信,为了模拟这种通信,我们可以开两个终端,分别运行 client.c 和 server.c。通过在client端发送信息:
与此同时,在server端接收到信息: