1、socket编程是什么?
Socket编程基于BSD(Berkeley Software Distribution)套接字接口,它提供了一种通用的网络编程接口,使得应用程序能够通过网络进行通信。
2、socket编程的基本原理包括以下几个步骤
(1)创建Socket:应用程序通过socket()系统调用创建一个Socket,该调用返回一个socket描述符,用于后续的通信。
(2)绑定地址:对于服务器端,需要使用bind()系统调用将socket绑定到一个特定的IP地址和端口上,以便客户端能够连接。
(3)监听连接请求(仅适用于TCP):对于TCP服务器,需要使用listen()系统调用开始监听客户端的连接请求。
(4)接受连接(仅适用于TCP):使用accept(),系统调用接受客户端的连接请求,返回一个新的socket描述符,用于与客户端进行通信。
(5)发起连接(仅适用于TCP客户端):对于TCP客户端,使用connenct(),系统调用向服务器段发起连接请求。
(6)发送和接收数据:通过send()和recv()系统调用(对于UDP,sendto()和recvfrom()系统调用)发送和接收数据。
(7)关闭连接:通信结束后,使用close()系统调用关闭socket连接。
3、socket模型
OSI模型
应用层:提供应用程序间通信
表示层:处理数据格式、数据加密等
会话层:建立、维护和管理会话
传输层:建立主机端对端连接
网络层:寻址和路由选择
数据链路层:提供介质访问、链路管理等
物理层:比特流传输
TCP/IP协议参考模型把所有的协议归类到四个抽象层中
应用层:TFTP、HTTP、SNMP、FTP、SMTP、DNS、Telnet等等
传输层:TCP、UDP
网络层:IP、ICMP等
数据链路层:SLIP、PPP
每一抽象层建立在低一层提供的服务上,并且为高一层提供服务。
Socket是在应用层和传输层之间的抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用以实现进程在网络中通信。Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过socket,才能使用TCP/IP协议。
4、socket的类型:数据流socket(TCP)和数据报(UDP)
TCP套接字:提供可靠的、面向连接的通信。(对讲机式门铃)
可靠连接:TCP套接字就像是带有对讲机的门铃。当你按下门铃,不仅能通知屋内有人来访,还能立即通过对讲机与访客对话,确认他们的身份和来意。
数据有序:使用这种门铃,你可以根据访客按门铃的顺序,一个接一个地与他们对话,不会出现混乱。
流量控制:如果一次性来的访客太多,对讲机系统会自动告诉一些访客稍等,以避免屋内变得过于拥挤。
拥塞控制:如果发现路上(网络)太拥挤,对讲机系统也会自动调整访客的到达速度,以防止过度拥堵。
错误恢复:如果对话中出现了问题(比如信号干扰),对讲机系统会尝试重新连接,确保信息准确传达。
UDP套接字:提供不可靠的、无连接的通信。(传统门铃)
无连接:UDP套接字就像是传统的门铃,只负责通知你有人来访,但不提供任何对话功能。
快速传送:因为没有对话确认的步骤,这种门铃允许访客快速按铃,适合不需要立即回复的情况。
不保证有序:由于没有对话功能,无法保证你与访客的交流顺序,可能需要你自己来维持秩序。
可能丢包:因为没有确认机制,有些按铃的信号可能因为各种原因丢失,需要你自己判断是否需要回应。
简单高效:这种门铃结构简单,使用方便,适合于不需要复杂交互的场景,如简单的信息提醒或广播通知。
TCP:适合于需要确保信息准确无误地传达的场合,比如正式的商务会议或者重要的信息交流。
UDP:适合于速度要求高、可以容忍一些误差的场合,比如实时的游戏数据传输或者电视直播。
5、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: IPv4
AF_INET6: IPv6
AF_UNIX: Unix域socket
type: 指定socket的通信方式,常用的有:
SOCK_STREAM: 面向连接的流式socket,如TCP
SOCK_DGRAM: 无连接的包式socket,如UDP
SOCK_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
(7)接收数据 —recv()
ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
参数:
sockfd: socket文件描述符。
buf: 接收数据的缓冲区。
len: 缓冲区的长度。
flags: 控制接收操作的标志位,通常为0。
返回值:
成功: 接收的字节数
失败: -1,并设置errno
0: 对应的socket连接已关闭
(8)关闭Socket —close()
int close(int sockfd); |
参数:
sockfd: 要关闭的socket文件描述符。
返回值:
成功: 0
失败: -1,并设置errno
(9)关闭连接方向 —shutdown()
int shutdown(int sockfd, int how); |
参数:
sockfd: socket文件描述符。
how: 指定如何关闭socket:
SHUT_RD: 关闭接收方向,不再读取数据。
SHUT_WR: 关闭发送方向,不再发送数据。
SHUT_RDWR: 关闭接收和发送方向。
返回值:
成功: 0
失败: -1,并设置errno
6、示例
下面是一个使用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。