Linux 网络编程-Socket
套接字
套接字(socket)是一种通信机制,就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。它可以明确地将客户端和服务器区分开来,可以实现一个或多个客户端到服务器的连接。
创建套接字
socket
系统调用创建一个套接字,并返回一个可以用来访问该套接字的描述符。
socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint.
The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process.
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int socket(int domain, int type, int protocol);
套接字属性
domain:
通信域,又被称为协议族,指定套接字通信中使用的通信介质
The domain argument specifies a communication domain; this selects the protocol family which will be used for communication.
如下是一些可以指定的协议族,定义在<sys/socket.h>
头文件中
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
最常用的套接字域有:AF_UNIX
和 AF_INET
。
AF_UNIX
用于通过 UNIX 和 Linux 文件系统实现的本地套接字,该域的底层协议就是文件输入/输出,AF_UNIX
域的套接字提供了一个可靠的双向通信路径。AF_INET
用于 UNIX 网络套接字,AF_INET
套接字可以用于通过包括因特网在内的 TCP/IP 网络进行通信的程序。
type:
指定套接字类型
The socket has the indicated type, which specifies the communication semantics.
常用的有 SOCK_STREAM
和 SOCK_DGRAM
SOCK_STREAM
提供有序的、可靠的、面向连接的双向字节流服务。对于 AF_INET
域套接字来说,它默认是通过一个 TCP
连接来提供这一特性的。
SOCK_DGRAM
支持数据报(固定最大长度的无连接、不可靠消息)。对于 AF_INET
域套接字,这种类型的通信是由 UDP
数据报来提供的。
从 Linux 2.6.27 开始,type 参数有第二个用途,除了指定套接字类型之外,它还可以包括以下任何值的按位或,以修改 socket() 的行为。
SOCK_NONBLOCK
在新打开的文件描述上设置 O_NONBLOCK 文件状态标志
SOCK_CLOEXEC
在新文件描述符上设置 close-on-exec (FD_CLOEXEC) 标志
protocol:
通信所用的协议一般由套接字类型和套接字域来决定,通常不需要选择,将该参数设置为 0 ,表示使用默认协议。
IPv4套接字创建
#include <sys/socket.h> #include <netinet/in.h> #include <netinet/ip.h> /* superset of previous */ tcp_socket = socket(AF_INET, SOCK_STREAM, 0); udp_socket = socket(AF_INET, SOCK_DGRAM, 0); raw_socket = socket(AF_INET, SOCK_RAW, protocol);
SOCK_STREAM
用于打开 tcp 套接字,SOCK_DGRAM
用于打开 udp 套接字,SOCK_RAW
用于打开 raw 套接字,以直接访问 IP 协议。protocol 是要接收或发送的 IP 0中的 IP 协议。
对于 tcp 套接字来说,protocol 唯一有效值是 0 和
IPPROTO_TCP
对于 udp 套接字来说,protocol 唯一有效值是 0 和
IPPROTO_UDP
对于
SOCK_RAW
,可以指定在 RFC 1700 分配的编号中定义的有效 IANA IP 协议。
套接字地址
每个套接字域都有其自己的地址格式。
AF_UNIX
域套接字地址由结构 sockaddr_un
来描述。
#define UNIX_PATH_MAX 108 struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ };
AF_INET
域中,套接字地址由结构 sockaddr_in
来指定,套接字由它的域、IP地址和端口号来完全确定。
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 */ };
IP 地址结构 in_addr
/* Internet address. */ struct in_addr { uint32_t s_addr; /* address in network byte order */ };
为套接字分配名称
要想让通过
socket
调用创建的套接字可以被其它进程使用,服务器程序就必须给该套接字命名,这样AF_UNIX
套接字就会关联到一个文件系统的路径名。AF_INET
套接字就会关联到一个 IP 和端口号。命名由
bind
系统调用实现,bind
系统调用把参数addr
中的地址分配给与sockfd
关联的未命名套接字,地址长度取决于套接字地址,由addrlen
传递,由于不同域的套接字地址结构不一致,因此bind
需要将一个特定的地址结构指针转换为指向通用地址结构类型 (struct sockaddr *
) 。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockaddr
结构的定义就像是:
struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
用于转换在 addr
中传递的结构指针,以避免编译警告。
创建套接字队列
为了能在套接字上接受进入的连接,服务器程序必须创建一个队列来保存未处理的请求,通过系统调用 listen 来完成。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int listen(int sockfd, int backlog);
The sockfd argument is a file descriptor that refers to a socket of type
SOCK_STREAM
orSOCK_SEQPACKET
.sockfd 参数是一个引用套接字类型为
SOCK_STREAM
或SOCK_SEQPACKET
的文件描述符。listen 函数将队列长度设置为 backlog 参数的值,在套接字队列中,等待处理的进入连接的个数最多不能超过这个数字,超过的连接将被拒绝,导致客户连接请求失败。
TCP 套接字上 backlog 参数的行为在 Linux 2.2 中发生了变化。现在它指定等待接受的完全建立的套接字的队列长度,而不是不完整的连接请求的数量。
接受连接
accept
系统调用与基于连接的套接字类型(SOCK_STREAM
、SOCK_SEQPACKET
)一起使用。
accept
系统调用只有当有客户程序试图连接到sockfd
参数指定的套接字上时才返回,这里客户是指 在套接字队列中排在第一个的未处理连接,它提取侦听套接字的挂起连接队列上的第一个连接请求。
accept
函数将创建一个新的套接字来与客户进行通信,并且返回新的套接字描述符,新创建的套接字不处于监听状态。 原来的套接字sockfd
不受此调用的影响。sockfd
指定的套接字必须事先由bind
调用命名,并且由listen
调用给它分配一个连接队列。连接客户的地址将被放入addr
所指向的结构中,如果不关心客户的地址,可以指定为空指针NULL
,同时addrlen
也应该为NULL
。如果队列中没有挂起的连接,并且套接字没有被标记为非阻塞,
accept()
会阻塞调用者,直到连接出现。如果套接字被标记为非阻塞并且队列中没有挂起的连接,accept()
将失败并返回EAGAIN
或EWOULDBLOCK
错误。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
请求连接
客户程序通过在一个未命名套接字和服务器监听套接字之间建立连接的方法来连接到服务器,通过
connect
系统调用来完成,参数sockfd
指定的套接字将连接到参数addr
指定的服务器套接字,sockfd
必须是通过socket
系统调用获得的一个有效的描述符。如果连接不能立刻建立,
connect
调用将阻塞一段不确定的超时时间。一旦这个超时时间到达,连接将被放弃,connect
调用失败。如果connect
调用 被一个信号中断,而该信号又得到了处理,connect
调用还是会失败(errno
被设置为EINTR
),但 连接尝试并不会被放弃,而是以异步方式继续建立,程序必须在此后进行检查以查看连接是否成功建立。
connect
调用的阻塞特性可以通过设置文件描述符的O_NONBLOCK
标志来改变,此时,如果连接不能立刻建立,connect
将失败并把errno
设置为EINPROGRESS
,而 连接将以异步的方式继续进行。
#include <sys/types.h> /* See NOTES */ #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
关闭套接字
通过 close 函数来终止客户和服务器上的套接字连接,就如同对底层文件描述符进行关闭一样。
#include <unistd.h> int close(int fd);
TCP Socket 编程流程
新创建的 TCP 套接字没有远程或本地地址,也没有完全指定。要创建传出 TCP 连接,请使用 connect 建立到另一个 TCP 套接字的连接。要接收新的传入连接,首先将套接字 bind 到本地地址和端口,然后调用 listen 使套接字进入侦听状态。之后,可以使用 accept 接受每个传入连接的新套接字。已成功调用 accept 或 connect 的套接字已完全指定并可传输数据。无法在侦听或尚未连接的套接字上传输数据。
TCP Socket 编程示例
tcp_srv.c
/************************************************************************* > File Name: tcp_srv.c > Author: shelmean > Mail: > Created Time: 2022年 04月 05日 星期二 15:04:03 CST ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <unistd.h> #include <errno.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #define LOCAL_HOST_ADDR "127.0.0.1" #define QUIT "quit" #define TCP_PORT (9527) #define BUFFER_LEN (1024) char buf[BUFFER_LEN] = {0}; /** * @Name - 初始化服务端套接字 * * @Return - 成功返回服务端套接字描述符,失败返回 -1 */ int init_server() { int sockfd = -1; struct sockaddr_in addr; bzero(&addr, sizeof(struct sockaddr_in)); /* Creating a socket */ if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket create failed!"); return -1; } /* Naming a socket */ addr.sin_family = AF_INET; addr.sin_addr.s_addr = inet_addr(LOCAL_HOST_ADDR); addr.sin_port = htons(TCP_PORT); if (bind(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0) { perror("bind error!"); return -1; } /* Creating a Socket Queue */ if (listen(sockfd, 5) < 0) { perror("listen error!"); return -1; } return sockfd; } int main(int argc, char *argv[]) { int srv_sockfd = -1; int confd = -1; int ret = 0; socklen_t cli_addr_len; struct sockaddr_in cli_addr; if ((srv_sockfd = init_server()) < 0) { exit(1); } bzero(&cli_addr, sizeof(struct sockaddr_in)); /* Accepting Connections */ cli_addr_len = sizeof(struct sockaddr_in); if ((confd = accept(srv_sockfd, (struct sockaddr *)&cli_addr, &cli_addr_len)) < 0) { perror("accept error."); exit(1); } /* recv */ while(1) { if ((ret = read(confd, buf, sizeof(buf))) < 0) { perror("read failed."); exit(1); } else if (ret == 0) { printf("client closed.\n"); break; } if (!strncmp(buf, QUIT, strlen(QUIT))) { printf("recv quilt cmd!\n"); break; } printf("recv bytes[%d]:%s", ret, buf); } close(srv_sockfd); return 0; }
tcp_cli.c
/************************************************************************* > File Name: tcp_cli.c > Author: shelmean > Mail: > Created Time: 2022年 04月 05日 星期二 15:04:03 CST ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <errno.h> #define LOCAL_HOST_ADDR "127.0.0.1" #define QUIT "quit" #define TCP_PORT (9527) #define BUFFER_LEN (1024) char buf[BUFFER_LEN] = {0}; int main(int argc, char *argv[]) { int sockfd = -1; struct sockaddr_in addr; bzero(&addr, sizeof(struct sockaddr_in)); /* Creating a socket */ if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket create failed!"); exit(1); } addr.sin_family = AF_INET; //IPv4 addr.sin_addr.s_addr = inet_addr(LOCAL_HOST_ADDR); addr.sin_port = htons(TCP_PORT); /* Requesting Connections */ if (connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0) { perror("connect failed!"); exit(1); } /* send to server */ while(1) { fgets(buf, sizeof(buf), stdin); write(sockfd, buf, strlen(buf) + 1); /* +1 send '\0' */ if (!strncmp(buf, QUIT, strlen(QUIT))) { break; } memset(buf, 0, sizeof(buf)); } close(sockfd); return 0; }
运行结果:
client 发送数据,服务器收到数据并打印出收到的数据。
参考
《Linux man page》
《Linux 程序设计第四版》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY