Socket(套接字)是一个网络编程概念,描述了一个通信端点(Endpoint),用于建立网络连接(Connection)并传输数据。
Linux Kernel 提供了一套面向 Socket 的网络编程框架,并通过提供一组标准的 System call APIs,使得开发者可以在 Userspace 中便捷的开发各种 Network Applications,例如:基于 HTTP 协议的 Web 服务器、基于 SMTP 协议的邮件服务器、基于 FTP 协议的文件服务器等等。
Linux Socket 网络编程框架主要由 3 大模块组成:
BSD Socket APIs(Berkeley Software Distribution Socket APIs),是面向 Userspace Application 的接口封装层,提供了一套兼容绝大部分网络通信协议族的标准 Socket APIs。
Socket API 的使用通常可以分为以下几个步骤:
需要注意的是,Socket API 并不是线程安全的,如果有多个线程同时使用了同一个 socket fd,则可能会导致数据传输错误或其他问题。为了避免这种情况,Application 需要进行适当的同步和数据处理。
Socket Abstraction Layer(Socket 抽象层),是 Socket API 的底层支撑,主要负责以下工作:
Socket Layer 与 Network Driver(网络设备驱动程序)之间通过 Socket Buffer(skb_buff)进行交互,当 Socket Layer 接收到 Application 的数据时,会将数据存储在 Socket Buffer 中,并将 Socket Buffer 传递给对应的 Sock Layer 进行处理。Struct Socket 和 Struct Sock 之间通过指针进行关联绑定,共同实现 Socket API 的功能。
// linux/include/linux/net.h
/**
typedef enum
{
SS_FREE=0; // 未分配
SS_UNCONNECTED; // 未连接到任何套接字
SS_CONNECTING; // 处于连接过程中
SS_CONNECTED; // 已经连接到另一个套接字
SS_DISCONNECTING; // 处于断开连接过程中
} socket_state;
Struct Sock 包含了 Socket 的各种底层执行状态和操作信息,例如:接收和发送缓冲区、套接字队列、套接字协议信息等。
// linux/include/net/sock.h
struct sock {
/* Socket family and type */
unsigned short family; // 协议族,如 AF_INET、AF_PACKET 等;
__u16 type; // 套接字类型,如 SOCK_STREAM、SOCK_DGRAM 等;
unsigned long flags; // 套接字标志,如 O_NONBLOCK、O_ASYNC 等;
/* Protocol specific elements of the socket */
struct proto *ops; // 协议特定操作函数集;
struct net_device *sk_net; // 套接字所在的网络设备;
/* Memory allocation cache */
kmem_cache_t *sk_slab; // 套接字内存分配缓存;
/* Socket state */
atomic_t refcnt; // 套接字引用计数;
struct mutex sk_lock; // 套接字锁,用于保护套接字状态的一致性;
/* Send and receive buffers */
struct sk_buff_head sk_receive_queue; // 接收队列,保存了等待接收的数据包;
struct sk_buff_head sk_write_queue; // 发送队列,保存了等待发送的数据包;
struct sk_buff *sk_send_head; // 发送缓冲区的头指针;
struct sk_buff *sk_send_tail; // 发送缓冲区的尾指针;
/* Receive queue */
struct sk_buff *sk_receive_skb; // 当前正在接收的数据包;
/* Transport specific fields */
__u32 sk_priority; // 套接字优先级;
struct dst_entry *sk_dst_cache; // 缓存的目标设备;
struct dst_entry *sk_dst_pending_confirm;
struct flowi sk_fl; // Flowi 结构体,保存了套接字相关的流信息;
struct sk_filter *sk_filter; // 过滤器;
struct sk_buff_head sk_async_wait_queue; // 异步等待队列;
/* Socket buffer allocations */
unsigned long sk_wmem_alloc; // 发送缓冲区已分配的内存;
unsigned long sk_omem_alloc;
/* User and kernel buffers */
struct socket_wq *sk_wq; // 套接字等待队列;
struct page_frag sk_frag; // 内存分配器的页片段;
int sk_forward_alloc; // 前向分配的字节数;
int sk_rxhash; // 套接字是否支持接收哈希。
};
Socket 支持广泛的 PFs,主要有以下 4 类:
值得注意的是,虽然不同的协议族都使用了同一套 Socket API,但也可能会存在一些特有的函数或者数据结构,用于实现协议族特有的功能。例如:
但是,这些特有的函数和数据结构通常不会影响套接字编程接口的基本使用方式和语法。
VFS Layer 属于 Linux VFS sub-system(虚拟文件子系统),提供了一组通用的 Linux File System Call APIs(SCI),使得 Application 可以使用相同的 API 来完成文件 I/O。
当 Application 使用 Socket API 发送或接收数据时,Socket Abstraction Layer 会借助 VFS Layer 来完成 Socket File System 的管理。例如:
PF_INET sockets 基于 IPv4v6 网络层协议,支持 TCP、UDP 等传输层协议。是 Linux 网络编程中最常用到的协议族。
函数功能:创建一个新的套接字,返回一个 int 类型的套接字文件描述符(作为 Linux 文件操作的句柄),用于后续的网络连接操作。
函数原型:
#include
int socket(int af, int type, int protocol);
示例:
// 创建 TCP 套接字
int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// 创建 UDP 套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
函数功能:用于设置 Socket 的选项值。
函数原型:
#include
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
将 Socket 与主机中的某个 IP:Port 绑定起来。
函数作用:将套接字与一个本地 IP:Port 绑定。通常用于服务端,以便在本地监听网络连接。函数原型:
#include
int bind(int sock, struct sockaddr *addr, socklen_t addrlen);
示例:
int tcp_socket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in tcp_socket_addr; // 定义 Server Socket Address
memset(&tcp_socket_addr, 0, sizeof(tcp_socket_addr)); // 初始化结构体内存
tcp_socket_addr.sin_family = PF_INET;
tcp_socket_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 定义本地 IP 地址
tcp_socket_addr.sin_port = htons(1314); // 定义本地 Port
bind(tcp_socket, (sockaddr *)&tcp_socket_addr, sizeof(sockaddr)); // 绑定
其中 sockaddr_in 结构类型的声明如下。使用时,需要先定义并初始化 sockaddr_in,然后再将它强制转化成 sockaddr 来使用。2 个结构体长度均为 16B,其中,sockaddr_in.sin_family 的 2B 存入 sockaddr.sa_family,剩下的 14B 存入 sockaddr.sa_data。
这样做是为了在后续的各种操作中可以更方便的处理 IP 地址和 Port 号。
#include
struct in_addr {
unsigned long a_addr;
}
struct sockaddr_in {
unsigned short sin_family; // 地址类型(2B)
unsigned short int sin_port; // 端口号(2B)
struct in_addr sin_addr; // IP 地址(4B)
unsigned char sin_zero[8]; // 填充空间(8B)
}
struct sockaddr {
unsigned short sa_family; // 地址类型(2B)
char sa_data[14]; // 协议地址(14B)
}
另外,IPv6 的结构体声明如下:
struct sockaddr_in6
{
sa_family_t sin6_family; // 地址类型,取值为 AF_INET6
in_port_t sin6_port; // 16 位端口号
uint32_t sin6_flowinfo; // IPv6 流信息
struct in6_addr sin6_addr; // 具体的 IPv6 地址
uint32_t sin6_scope_id; // 接口范围 ID
};
如果 sock_addr.sin_port 赋值为 0,或者没有调用 bind(),而直接调用 listen(),那么 Kernel 会自动为 Socket 临时分配一个 Port。此时需要调用 getsockname() 来获取具体的端口信息。
getsockname(httpd, (struct sockaddr *)&name, &namelen);
ntohs(name.sin_port);
函数作用:开始监听来自远程主机的连接请求。通常用于服务器端,在套接字上等待来自客户端的连接请求。
函数原型:
#include
int listen(int sock, int backlog);
函数作用:建立与远程主机的连接。通常用于客户端,以便连接到远程服务器。函数原型:
#include
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen);
示例:
int cli_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in server_sock_addr; // 定义 Server Socket Address
memset(&server_sock_addr, 0, sizeof(server_sock_addr)); // 初始化结构体内存
server_sock_addr.sin_family = PF_INET;
server_sock_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 定义本地 IP 地址
server_sock_addr.sin_port = htons(1314); // 定义本地 Port
connect(cli_socket, (sockaddr *)&server_sock_addr, sizeof(sockaddr));
函数作用:接受一个连接请求,返回一个新的、表示客户端的 Socket 文件描述符,作为服务端和客户端之间发送与接收操作的句柄。通常用于服务器端,用于接收客户端的连接请求。
函数原型:
#include
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);
示例:
// 返回一个新的套接字,用于后续的发送和接收
int cli_socket = accept(server_socket, (sockeraddr *)&cli_socket_addr, &len);
函数作用:用于将一个 Sock Addr 转换为对应的 Hostname 或 Service name,以便于记录日志或者显示给用户。
函数原型:
#include
int getnameinfo(const struct sockaddr *addr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);
recv() 和 send() 函数,用于在 TCP Socket 中进行数据读写,属于阻塞式 I/O(Blocking I/O)模式,即:如果没有可读数据或者对端的接收缓冲区已满,则函数将一直等待直到有数据可读或者对端缓冲区可写。
recv():从套接字接收数据。
#include
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
send():向套接字发送数据。
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
recvfrom() 和 sendto() 函数,用于在 UDP Socket 中进行数据读写以及获取对端地址。这两个函数在使用时需要指定对端的 IP:Port。
recvfrom():
#include
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockadr *from, socklen_t *addrlen);
sendto():
#include
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
recvmsg() 和 sendmsg() 函数,用于在 TCP 和 UDP Socket 中进行数据读写,不仅可以读写数据,还可以读写对端地址、辅助数据等信息。
recvmsg():
#include
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
sendmsg()
#include
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
msghdr 结构体定义如下:
struct msghdr {
/* 指定接收或发送数据的对端地址,可以为 NULL 或 0,表示不需要使用对端地址。*/
void msg_name; / optional address /
socklen_t msg_namelen; / size of address */
/* 指定接收或发送数据的缓冲区和缓冲区大小,可以使用多个缓冲区同时接收或发送数据。*/
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
/* 指定一些附加的控制信息,可以为 NULL 或 0。*/
void msg_control; / ancillary data, see below /
size_t msg_controllen; / ancillary data buffer len */
/* 指定函数的行为,例如是否需要接收带外数据等。/
int msg_flags; / flags on received message */
};
函数作用:关闭套接字连接。函数原型:
#include
int close(int fd);
#include
#include
#include
#include
#include
#include
#include
#define ERR_MSG(err_code) do {
err_code = errno;
fprintf(stderr, "ERROR code: %d n", err_code);
perror("PERROR message");
} while (0)
const int BUF_LEN = 100;
int main(void)
{
/* 配置 Server Sock 信息。*/
struct sockaddr_in srv_sock_addr;
memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
srv_sock_addr.sin_family = AF_INET;
srv_sock_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 即 0.0.0.0 表示监听本机所有的 IP 地址。
srv_sock_addr.sin_port = htons(6666);
/* 创建 Server Socket。*/
int srv_socket_fd = 0;
if (-1 == (srv_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))) {
printf("Create socket file descriptor ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 设置 Server Socket 选项。*/
int optval = 1;
if (setsockopt(srv_socket_fd,
SOL_SOCKET, // 表示套接字选项的协议层。
SO_REUSEADDR, // 表示在绑定地址时允许重用本地地址。这样做的好处是,当服务器进程崩溃或被关闭时,可以更快地重新启动服务器,而不必等待一段时间来释放之前使用的套接字。
&optval,
sizeof(optval)) < 0)
{
printf("Set socket options ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 绑定 Socket 与 Sock Address 信息。*/
if (-1 == bind(srv_socket_fd,
(struct sockaddr *)&srv_sock_addr,
sizeof(srv_sock_addr)))
{
printf("Bind socket ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 开始监听 Client 发出的连接请求。*/
if (-1 == listen(srv_socket_fd, 10))
{
printf("Listen socket ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 初始化 Client Sock 信息存储变量。*/
struct sockaddr cli_sock_addr;
memset(&cli_sock_addr, 0, sizeof(cli_sock_addr));
int cli_sockaddr_len = sizeof(cli_sock_addr);
int cli_socket_fd = 0;
int recv_len = 0;
char buff[BUF_LEN] = {0};
/* 永远接受 Client 的连接请求。*/
while (1)
{
if (-1 == (cli_socket_fd = accept(srv_socket_fd,
(struct sockaddr *)(&cli_sock_addr), // 填充 Client Sock 信息。
(socklen_t *)&cli_sockaddr_len)))
{
printf("Accept connection from client ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 接收指定 Client Socket 发出的数据,*/
if ((recv_len = recv(cli_socket_fd, buff, BUF_LEN, 0)) < 0)
{
printf("Receive from client ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
printf("Recevice data from client: %sn", buff);
/* 将收到的数据重新发送给指定的 Client Socket。*/
send(cli_socket_fd, buff, recv_len, 0);
printf("Send data to client: %sn", buff);
/* 每处理完一次 Client 请求,即关闭连接。*/
close(cli_socket_fd);
memset(buff, 0, BUF_LEN);
}
close(srv_socket_fd);
return EXIT_SUCCESS;
}
#include
#include
#include
#include
#include
#include
#include
#define ERR_MSG(err_code) do {
err_code = errno;
fprintf(stderr, "ERROR code: %d n", err_code);
perror("PERROR message");
} while (0)
const int BUF_LEN = 100;
int main(void)
{
/* 配置 Server Sock 信息。*/
struct sockaddr_in srv_sock_addr;
memset(&srv_sock_addr, 0, sizeof(srv_sock_addr));
srv_sock_addr.sin_family = AF_INET;
srv_sock_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
srv_sock_addr.sin_port = htons(6666);
int cli_socket_fd = 0;
char send_buff[BUF_LEN];
char recv_buff[BUF_LEN];
/* 永循环从终端接收输入,并发送到 Server。*/
while (1) {
/* 创建 Client Socket。*/
if (-1 == (cli_socket_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)))
{
printf("Create socket ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 连接到 Server Sock 信息指定的 Server。*/
if (-1 == connect(cli_socket_fd,
(struct sockaddr *)&srv_sock_addr,
sizeof(srv_sock_addr)))
{
printf("Connect to server ERROR.n");
ERR_MSG(errno);
exit(EXIT_FAILURE);
}
/* 从 stdin 接收输入,再发送到建立连接的 Server Socket。*/
fputs("Send to server > ", stdout);
fgets(send_buff, BUF_LEN, stdin);
send(cli_socket_fd, send_buff, BUF_LEN, 0);
memset(send_buff, 0, BUF_LEN);
/* 从建立连接的 Server 接收数据。*/
recv(cli_socket_fd, recv_buff, BUF_LEN, 0);
printf("Recevice from server: %sn", recv_buff);
memset(recv_buff, 0, BUF_LEN);
/* 每次 Client 请求和响应完成后,关闭连接。*/
close(cli_socket_fd);
}
return EXIT_SUCCESS;
}
编译:
$ gcc -g -std=c99 -Wall tcp_server.c -o tcp_server
$ gcc -g -std=c99 -Wall tcp_client.c -o tcp_client
运行:
$ ./tcp_server
$ netstat -lpntu | grep 6666
tcp 0 0 0.0.0.0:6666 0.0.0.0:* LISTEN 28675/./tcp_server
$ ./tcp_client
·
#include
#include
#include
#include
#include
#include
#include
#define BUF_LEN 100
int main(void)
{
int ServerFd;
char Buf[BUF_LEN] = {0};
struct sockaddr ClientAddr;
struct sockaddr_in ServerSockAddr;
int addr_size = 0;
int optval = 1;
/* 创建 UDP 服务端 Socket */
if ( -1 == (ServerFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)))
{
printf("socket error!n");
exit(1);
}
/* 设置服务端信息 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr)); // 给结构体ServerSockAddr清零
ServerSockAddr.sin_family = AF_INET; // 使用IPv4地址
ServerSockAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 自动获取IP地址
ServerSockAddr.sin_port = htons(1314); // 端口
// 设置地址和端口号可以重复使用
if (setsockopt(ServerFd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
{
printf("setsockopt error!n");
exit(1);
}
/* 绑定操作,绑定前加上上面的socket属性可重复使用地址 /
if (-1 == bind(ServerFd, (struct sockaddr)&ServerSockAddr, sizeof(ServerSockAddr)))
{
printf("bind error!n");
exit(1);
}
addr_size = sizeof(ClientAddr);
while (1)
{
/* 接受客户端的返回数据 */
int str_len = recvfrom(ServerFd, Buf, BUF_LEN, 0, &ClientAddr, &addr_size);
printf("客户端发送过来的数据为:%sn", Buf);
/* 发送数据到客户端 */
sendto(ServerFd, Buf, str_len, 0, &ClientAddr, addr_size);
/* 清空缓冲区 */
memset(Buf, 0, BUF_LEN);
}
close(ServerFd);
return 0;
}
#include
#include
#include
#include
#include
#include
#define BUF_LEN 100
int main(void)
{
int ClientFd;
char Buf[BUF_LEN] = {0};
struct sockaddr ServerAddr;
int addr_size = 0;
struct sockaddr_in ServerSockAddr;
/* 创建客户端socket */
if (-1 == (ClientFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)))
{
printf("socket error!n");
exit(1);
}
/* 向服务器发起请求 */
memset(&ServerSockAddr, 0, sizeof(ServerSockAddr));
ServerSockAddr.sin_family = PF_INET;
ServerSockAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
ServerSockAddr.sin_port = htons(1314);
addr_size = sizeof(ServerAddr);
while (1)
{
printf("请输入一个字符串,发送给服务端:");
gets(Buf);
/* 发送数据到服务端 /
sendto(ClientFd, Buf, strlen(Buf), 0, (struct sockaddr)&ServerSockAddr, sizeof(ServerSockAddr));
/* 接受服务端的返回数据 */
recvfrom(ClientFd, Buf, BUF_LEN, 0, &ServerAddr, &addr_size);
printf("服务端发送过来的数据为:%sn", Buf);
memset(Buf, 0, BUF_LEN); // 重置缓冲区
}
close(ClientFd); // 关闭套接字
return 0;
}
运行:
$ netstat -lpntu | grep 1314
udp 0 0 0.0.0.0:1314 0.0.0.0:* 29729/./udp_server
审核编辑 黄宇
全部0条评论
快来发表一下你的评论吧 !