这篇是我在学习Socket(套接字)编程过程中的笔记整理,收集学习了一些网上的文章,但我想要用我习惯的方式(用门铃来比喻套接字)记录下学习的内容,方便日后查看。
一、Socket编程是什么?
1、概念
Socket编程是一种实现网络通信的编程技术,它允许不同主机上的应用进程之间进行数据交换。
想象一下,家门上的门铃其实是一个Socket。每当有人(数据包)按下门铃,不管他们在哪儿,只要还在地球上,你都能知道有人来访。在网络世界里,Socket就像这个门铃,它允许两个程序不管距离多远,都能相互“拜访”和交换信息。
2、Socket的类型:流式Socket(TCP)和数据报Socket(UDP)
TCP套接字:提供可靠的、面向连接的通信。(对讲机式门铃)
-
可靠连接:TCP套接字就像是带有对讲机的门铃。当你按下门铃,不仅能通知屋内有人来访,还能立即通过对讲机与访客对话,确认他们的身份和来意。
-
数据有序:使用这种门铃,你可以根据访客按门铃的顺序,一个接一个地与他们对话,不会出现混乱。
-
流量控制:如果一次性来的访客太多,对讲机系统会自动告诉一些访客稍等,以避免屋内变得过于拥挤。
-
拥塞控制:如果发现路上(网络)太拥挤,对讲机系统也会自动调整访客的到达速度,以防止过度拥堵。
-
错误恢复:如果对话中出现了问题(比如信号干扰),对讲机系统会尝试重新连接,确保信息准确传达。
UDP套接字:提供不可靠的、无连接的通信。(传统门铃)
-
无连接:UDP套接字就像是传统的门铃,只负责通知你有人来访,但不提供任何对话功能。
-
快速传送:因为没有对话确认的步骤,这种门铃允许访客快速按铃,适合不需要立即回复的情况。
-
不保证有序:由于没有对话功能,无法保证你与访客的交流顺序,可能需要你自己来维持秩序。
-
可能丢包:因为没有确认机制,有些按铃的信号可能因为各种原因丢失,需要你自己判断是否需要回应。
-
简单高效:这种门铃结构简单,使用方便,适合于不需要复杂交互的场景,如简单的信息提醒或广播通知。
- TCP:适合于需要确保信息准确无误地传达的场合,比如正式的商务会议或者重要的信息交流。
- UDP:适合于速度要求高、可以容忍一些误差的场合,比如实时的游戏数据传输或者电视直播。
二、Socket API理解
Socket编程通常遵循以下步骤:
-
创建Socket——
socket()
:创建一个socket对象。 -
绑定Socket——
bind()
:一旦socket被创建,通常需要将其绑定到一个特定的网络地址和端口上,这样它就可以监听进入的连接请求。
绑定过程就像是给门铃安装一个门牌号码。这个号码包括了网络地址(IP地址)和门铃的“房间号”(端口号)。这样,当有人按门铃时,不仅知道有人来访,还能知道这个访客是想要访问家里的哪个“房间”(服务)。 -
监听连接(对于服务器端)——
listen()
:服务器端的socket需要监听进入的连接请求。
当门铃安装好并有了门牌号码后,就可以开始“监听”门铃了。在网络世界里,监听就像是你站在门旁,等待门铃响起,这意味着有人在请求进入。 -
连接到服务器(对于客户端)——
connect()
:客户端需要使用connect()
函数来连接到服务器的socket。
对于客户端来说,连接就像是站在门外按响邻居家的门铃。当你按下门铃并等待邻居响应时,你实际上是在发送一个连接请求。 -
接受连接(对于服务器端)——
accept()
:当服务器监听到连接请求时,使用accept()
函数来接受连接,这将创建一个新的socket用于与客户端通信。
当服务器“听到”门铃并“查看”门牌号码后,它会决定是否开门。接受连接就像是打开门,欢迎来访的客人进入。 -
数据传输——
send(),recv()
:一旦socket连接建立,就可以使用send()
和recv()
函数(或类似的函数,如write()
和read()
)来发送和接收数据。 -
关闭Socket——
close()
:通信完成之后,需要通过close()
函数关闭socket来释放资源。
三、Socket API原型
Socket编程包含的头文件:
#include <sys/types.h>
#include <sys/socket.h>
1、创建Socket —socket()
原型:
int socket(int domain, int type, int protocol);
参数:
domain
: 指定socket的协议域,常用的有:AF_INET
: IPv4AF_INET6
: IPv6AF_UNIX
: Unix域socket
type
: 指定socket的通信方式,常用的有:SOCK_STREAM
: 面向连接的流式socket,如TCPSOCK_DGRAM
: 无连接的包式socket,如UDPSOCK_SEQPACKET
: 面向序列包的socket
protocol
: 指定协议,通常设置为0以使用默认协议。
返回值:
- 成功: 新创建的socket的文件描述符(fd)
- 失败: -1,并设置
errno
以指示错误类型
2. 绑定Socket —bind()
原型:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
: 由socket()
创建的socket文件描述符。addr
: 指向sockaddr
结构体的指针,该结构体包含了要绑定的地址信息。addrlen
:sockaddr
结构体的大小。
返回值:
- 成功: 0
- 失败: -1,并设置
errno
3. 监听连接 —listen()
原型:
int listen(int sockfd, int backlog);
参数:
sockfd
: 已绑定到地址的socket文件描述符。backlog
: 指定内核用于存放未连接但已排队的连接请求的最大数量。
返回值:
- 成功: 0
- 失败: -1,并设置
errno
4. 接受连接 —accept()
原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd
: 监听socket的文件描述符。addr
: 如果非NULL,接受连接后,该参数将被填充为客户端的地址信息。addrlen
: 传入时,指向存放addr
地址结构体长度的变量的指针;传出时,返回实际的地址长度。
返回值:
- 成功: 一个新的socket文件描述符,用于与客户端通信。
- 失败: -1,并设置
errno
5. 建立连接 —connect()
原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd
: 客户端socket文件描述符。addr
: 指向包含服务器地址信息的sockaddr
结构体的指针。addrlen
:sockaddr
结构体的大小。
返回值:
- 成功: 0
- 失败: -1,并设置
errno
6. 数据传输
发送数据 —send()
原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
sockfd
: socket文件描述符。buf
: 指向要发送数据的缓冲区的指针。len
: 要发送数据的长度。flags
: 控制发送操作的标志位,通常为0。
返回值:
- 成功: 发送的字节数
- 失败: -1,并设置
errno
接收数据 —recv()
原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
sockfd
: socket文件描述符。buf
: 接收数据的缓冲区。len
: 缓冲区的长度。flags
: 控制接收操作的标志位,通常为0。
返回值:
- 成功: 接收的字节数
- 失败: -1,并设置
errno
- 0: 对应的socket连接已关闭
7. 关闭Socket —close()
原型:
int close(int sockfd);
参数:
sockfd
: 要关闭的socket文件描述符。
返回值:
- 成功: 0
- 失败: -1,并设置
errno
8. 关闭连接方向 —shutdown()
原型:
int shutdown(int sockfd, int how);
参数:
sockfd
: socket文件描述符。how
: 指定如何关闭socket:SHUT_RD
: 关闭接收方向,不再读取数据。SHUT_WR
: 关闭发送方向,不再发送数据。SHUT_RDWR
: 关闭接收和发送方向。
返回值:
- 成功: 0
- 失败: -1,并设置
errno
四、示例
下面是一个使用TCP协议的socket服务器端的示例,主要是创建一个简单的echo服务器,该服务器接收客户端发送的数据并将其回传给客户端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080 // 服务器监听的端口号
int main() {
int server_fd, new_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[1024] = {0};
// 创建socket(socket)
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 初始化服务器地址结构体(bind之前的准备)
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // 地址族
server_addr.sin_addr.s_addr = INADDR_ANY; // 服务器IP地址(任意)
server_addr.sin_port = htons(PORT); // 服务器端口
// 将socket绑定到地址(bind)
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 开始监听传入连接(listen)
if (listen(server_fd, 5) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
printf("Server is listening on port %d...\n", PORT);
// 接受客户端连接(accept)
new_socket = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (new_socket < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
// 接收客户端发送的数据(recv)
int valread = recv(new_socket, buffer, 1024, 0);
if (valread < 0) {
perror("recv failed");
close(new_socket);
exit(EXIT_FAILURE);
}
// 发送数据回客户端(send)
send(new_socket, buffer, strlen(buffer), 0);
// 关闭新的socket(close)
close(new_socket);
// 关闭监听socket
close(server_fd);
return 0;
}
在这个例子当中,服务器首先创建了一个socket,然后将其绑定到本地的8080端口。接着,服务器进入监听状态,等待客户端的连接请求。当客户端连接时,服务器接受这个连接,创建一个新的socket用于与客户端通信。服务器接收客户端发送的消息,然后使用相同的消息进行回应。最后,服务器关闭用于通信的socket以及监听socket。
需要注意的是,这个例子是一个简化的版本,仅仅是为了展示Socket API 的使用,没有包含错误处理和多线程/异步处理的逻辑,这些在实际的服务器应用中是必需要有的。而且,服务器在接收到客户端的消息后立即关闭了连接,实际的服务器可能会维护一个持久的连接或同时处理多个连接。