Socket编程基础(易懂版)

这篇是我在学习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
#include
1、创建Socket —socket()
原型:
int socket(int domain, int type, int protocol);
参数:
domain: 指定socket的协议域,常用的有:
AF_INET: IPv4AF_INET6: IPv6AF_UNIX: Unix域sockettype: 指定socket的通信方式,常用的有:
SOCK_STREAM: 面向连接的流式socket,如TCPSOCK_DGRAM: 无连接的包式socket,如UDPSOCK_SEQPACKET: 面向序列包的socketprotocol: 指定协议,通常设置为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,并设置errno3. 监听连接 —listen()
原型:
int listen(int sockfd, int backlog);
参数:
sockfd: 已绑定到地址的socket文件描述符。backlog: 指定内核用于存放未连接但已排队的连接请求的最大数量。返回值:
成功: 0失败: -1,并设置errno4. 接受连接 —accept()
原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
sockfd: 监听socket的文件描述符。addr: 如果非NULL,接受连接后,该参数将被填充为客户端的地址信息。addrlen: 传入时,指向存放addr地址结构体长度的变量的指针;传出时,返回实际的地址长度。返回值:
成功: 一个新的socket文件描述符,用于与客户端通信。失败: -1,并设置errno5. 建立连接 —connect()
原型:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
sockfd: 客户端socket文件描述符。addr: 指向包含服务器地址信息的sockaddr结构体的指针。addrlen: sockaddr结构体的大小。返回值:
成功: 0失败: -1,并设置errno6. 数据传输
发送数据 —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,并设置errno0: 对应的socket连接已关闭7. 关闭Socket —close()
原型:
int close(int sockfd);
参数:
sockfd: 要关闭的socket文件描述符。返回值:
成功: 0失败: -1,并设置errno8. 关闭连接方向 —shutdown()
原型:
int shutdown(int sockfd, int how);
参数:
sockfd: socket文件描述符。how: 指定如何关闭socket:
SHUT_RD: 关闭接收方向,不再读取数据。SHUT_WR: 关闭发送方向,不再发送数据。SHUT_RDWR: 关闭接收和发送方向。返回值:
成功: 0失败: -1,并设置errno四、示例
下面是一个使用TCP协议的socket服务器端的示例,主要是创建一个简单的echo服务器,该服务器接收客户端发送的数据并将其回传给客户端。
#include
#include
#include
#include
#include
#include
#include
#include
#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 的使用,没有包含错误处理和多线程/异步处理的逻辑,这些在实际的服务器应用中是必需要有的。而且,服务器在接收到客户端的消息后立即关闭了连接,实际的服务器可能会维护一个持久的连接或同时处理多个连接。