最基本的 Socket 模型
参考这次答应我,一举拿下 I/O 多路复用! (qq.com)
Socket编程详解-CSDN博客
Socket是一种通信机制,通过它可以在不同主机之间进行数据交换。在Socket编程中,有两种常见的通信模式:客户端-服务器模式和点对点模式。它基于TCP/IP协议栈,并使用IP地址和端口号来标识通信的目标。
Socket 的中文名叫作套接字,欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。很像弄了一根网线,一头插在客户端,一头插在服务端,然后进行通信。“IP地址+端口号”就对应一个socket。
在网络通信中,套接字一定是成对出现的。一端的发送缓冲区对应对端的接收缓冲区。我们使用同一个文件描述符索发送缓冲区和接收缓冲区。
服务端首先调用socket()函数,创建网络协议为 IPv4,以及传输协议为 TCP 的 Socket ,接着调用bind()函数,给这个 Socket 绑定一个 IP 地址和端口,绑定这两个的目的是什么?
-
绑定端口的目的:当内核收到 TCP 报文,通过 TCP 头里面的端口号,来找到我们的应用程序,然后把数据传递给我们。
-
绑定 IP 地址的目的:一台机器是可以有多个网卡的,每个网卡都有对应的 IP 地址,当绑定一个网卡时,内核在收到该网卡上的包,才会发给我们;
绑定完 IP 地址和端口后,就可以调用listen()函数进行监听,此时对应 TCP 状态图中的 listen,如果我们要判定服务器中一个网络程序有没有启动,可以通过 netstate 命令查看对应的端口号是否有被监听。
服务端进入了监听状态后,通过调用accept()函数,来从内核获取客户端的连接,如果没有客户端连接,则会阻塞等待客户端连接的到来。
那客户端是怎么发起连接的呢?客户端在创建好 Socket 后,调用connect()函数发起连接,该函数的参数要指明服务端的 IP 地址和端口号,然后万众期待的 TCP 三次握手就开始了。
在 TCP 连接的过程中,服务器的内核实际上为每个 Socket 维护了两个队列:
-
一个是还没完全建立连接的队列,称为 TCP 半连接队列,这个队列都是没有完成三次握手的连接,此时服务端处于syn_rcvd的状态;
-
一个是一件建立连接的队列,称为 TCP 全连接队列,这个队列都是完成了三次握手的连接,此时服务端处于established状态;
当 TCP 全连接队列不为空后,服务端的accept()函数,就会从内核中的 TCP 全连接队列里拿出一个已经完成连接的 Socket 返回应用程序,后续数据传输都用这个 Socket。
注意,监听的 Socket 和真正用来传数据的 Socket 是两个:
-
一个叫作监听 Socket;
-
一个叫作已连接 Socket;
连接建立后,客户端和服务端就开始相互传输数据了,双方都可以通过read()和write()函数来读写数据。
至此, TCP 协议的 Socket 程序的调用过程就结束了
基于 Linux 一切皆文件的理念,在内核中 Socket 也是以「文件」的形式存在的,也是有对应的文件描述符(内核可以通过文件描述符找到对应打开的文件)
相关知识
参考CC++ socket网络编程扫盲篇_c++端口号的数据类型-CSDN博客
大小端存储
大小端是针对非单字节数据的存取,比如short型,int型等。大端存储是将数据的高位存储在内存的低地址处,小端存储是将数据的低位存储在内存的低地址处。
为什么会有大小端?
简单点说就是硬件厂商各有所好,并没有统一的约定制作制作哪一个,大端的优势在于第一个字节就是高位,很容易判断正负性。小端的优势在于第一个字节是低位,最后一个字节是高位,可以依次取出相应的字节进行运算,并且最终会把符号位刷新,这样运算起来更高效。
内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分。网络数据流同样有大端小端之分,那么如何定义网络数据流的地址呢?发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定:网络数据流应采用大端字节序,即低地址高字节。
IP地址(IP Address)
计算机分布在世界各地,要想和它们通信,必须要知道确切的位置。确定计算机位置的方式有多种,IP 地址是最常用的,例如,114.114.114.114 是国内第一个、全球第三个开放的 DNS 服务地址,127.0.0.1 是本机地址。
其实,我们的计算机并不知道 IP 地址对应的地理位置,当要通信时,只是将 IP 地址封装到要发送的数据包中,交给路由器去处理。路由器有非常智能和高效的算法,很快就会找到目标计算机,并将数据包传递给它,完成一次单向通信。
目前大部分软件使用 IPv4 地址,但 IPv6 也正在被人们接受,尤其是在教育网中,已经大量使用。
端口(Port)
有了 IP 地址,虽然可以找到目标计算机,但仍然不能进行通信。一台计算机可以同时提供多种网络服务,例如Web服务、FTP服务(文件传输服务)、SMTP服务(邮箱服务)等,仅有 IP 地址,计算机虽然可以正确接收到数据包,但是却不知道要将数据包交给哪个网络程序来处理,所以通信失败。
为了区分不同的网络程序,计算机会为每个网络程序分配一个独一无二的端口号(Port Number),例如,Web服务的端口号是 80,FTP 服务的端口号是 21,SMTP 服务的端口号是 25。
端口(Port)是一个虚拟的、逻辑上的概念。可以将端口理解为一道门,数据通过这道门流入流出,每道门有不同的编号,就是端口号。如下图所示:
协议(Protocol)
协议(Protocol)就是网络通信的约定,通信的双方必须都遵守才能正常收发数据。协议有很多种,例如 TCP、UDP、IP 等,通信的双方必须使用同一协议才能通信。协议是一种规范,由计算机组织制定,规定了很多细节,例如,如何建立连接,如何相互识别等。
协议仅仅是一种规范,必须由计算机软件来实现。例如 IP 协议规定了如何找到目标计算机,那么各个开发商在开发自己的软件时就必须遵守该协议,不能另起炉灶。
所谓协议族(Protocol Family),就是一组协议(多个协议)的统称。最常用的是 TCP/IP 协议族,它包含了 TCP、IP、UDP、Telnet、FTP、SMTP 等上百个互为关联的协议,由于 TCP、IP 是两种常用的底层协议,所以把它们统称为 TCP/IP 协议族。
数据传输方式
计算机之间有很多数据传输方式,各有优缺点,常用的有两种:SOCK_STREAM(使用TCP协议) 和 SOCK_DGRAM(使用UDP协议)。
SOCK_STREAM 表示面向连接的数据传输方式。数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送,但效率相对较慢。常见的 http 协议就使用 SOCK_STREAM 传输数据,因为要确保数据的正确性,否则网页不能正常解析。
SOCK_DGRAM 表示无连接的数据传输方式。计算机只管传输数据,不作数据校验,如果数据在传输中损坏,或者没有到达另一台计算机,是没有办法补救的。也就是说,数据错了就错了,无法重传。因为 SOCK_DGRAM 所做的校验工作少,所以效率比 SOCK_STREAM 高。
QQ 视频聊天和语音聊天就使用 SOCK_DGRAM 传输数据,因为首先要保证通信的效率,尽量减小延迟,而数据的正确性是次要的,即使丢失很小的一部分数据,视频和音频也可以正常解析,最多出现噪点或杂音,不会对通信质量有实质的影响。
注意:SOCK_DGRAM 没有想象中的糟糕,不会频繁的丢失数据,数据错误只是小概率事件。
有可能多种协议使用同一种数据传输方式,所以在 socket 编程中,需要同时指明数据传输方式和协议。
综上所述:IP地址和端口能够在广袤的互联网中定位到要通信的程序,协议和数据传输方式规定了如何传输数据,有了这些,两台计算机就可以通信了。
Socket编程基本步骤
参考Socket编程详解:从基本概念到实例应用(TCP|UDP C语言实例详解)-CSDN博客
常用函数的原型和参数用法详解:
要使用Socket函数之前要先包含两个头文件
#include <sys/types.h>
#include <socket.h>
1.socket()函数
int socket(int domain, int type, int protocol);
domain:地址族,通常为AF_INET(IPv4)或AF_INET6(IPv6)。
type:套接字类型,可以是SOCK_STREAM(TCP)或SOCK_DGRAM(UDP)。
protocol:协议,通常为0,表示自动选择合适的协议。
2.bind()函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符。
addr:指向本地地址结构体的指针。
addrlen:地址结构体的长度。
3.listen()函数
int listen(int sockfd, int backlog);
sockfd:套接字描述符。
backlog:待连接队列的最大长度。
4.accept()函数
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:套接字描述符。
addr:指向客户端地址结构体的指针,用于存储客户端的地址信息。
addrlen:指向地址结构体长度的指针。
5.connect()函数
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd:套接字描述符。
addr:指向目标地址结构体的指针。
addrlen:地址结构体的长度。
6.send()函数
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
sockfd:套接字描述符。
buf:指向要发送数据的缓冲区的指针。
len:要发送的数据的长度。
flags:发送标志,通常为0。
7.recv()函数
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sockfd:套接字描述符。
buf:指向接收数据的缓冲区的指针。
len:要接收的数据的最大长度。
flags:接收标志,通常为0。
8.sendto()函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
sockfd:套接字描述符。
buf:指向要发送数据的缓冲区的指针。
len:要发送的数据的长度。
flags:发送标志,通常为0。
dest_addr:指向目标地址结构体的指针。
addrlen:目标地址结构体的长度。
9.recvfrom()函数
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
sockfd:套接字描述符。
buf:指向接收数据的缓冲区的指针。
len:要接收的数据的最大长度。
flags:接收标志,通常为0。
src_addr:指向源地址结构体的指针,用于存储源地址信息。
addrlen:指向地址结构体长度的指针。
10.close()函数
int close(int sockfd);
sockfd:套接字描述符。
通过使用这些函数,可以实现TCP和UDP通信的服务器端和客户端程序。TCP提供可靠的、面向连接的通信,适用于需要确保数据可靠性和顺序性的场景,而UDP提供无连接的通信,适用于实时性要求较高但可靠性要求较低的场景。
TCP开发流程:
1.服务器端:
- 创建套接字:int serverSocket = socket(AF_INET, SOCK_STREAM, 0);
- 绑定地址:struct sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons(port); serverAddress.sin_addr.s_addr = INADDR_ANY; bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
- 监听连接:listen(serverSocket, backlog);
- 接受连接:struct sockaddr_in clientAddress; int clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddress, sizeof(clientAddress));
- 发送和接收数据:send(clientSocket, buffer, size, 0); recv(clientSocket, buffer, size, 0);
- 关闭连接:close(clientSocket); close(serverSocket);
2.客户端:
- 创建套接字:int clientSocket = socket(AF_INET, SOCK_STREAM, 0);
- 连接服务器:struct sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons(port); serverAddress.sin_addr.s_addr = inet_addr(serverIP); connect(clientSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
- 发送和接收数据:send(clientSocket, buffer, size, 0); recv(clientSocket, buffer, size, 0);
- 关闭连接:close(clientSocket);
UDP开发流程:
1.服务器端:
- 创建套接字:int serverSocket = socket(AF_INET, SOCK_DGRAM, 0);
- 绑定地址:struct sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons(port); serverAddress.sin_addr.s_addr = INADDR_ANY; bind(serverSocket, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
- 接收和发送数据:recvfrom(serverSocket, buffer, size, 0, (struct sockaddr*)&clientAddress, sizeof(clientAddress)); sendto(serverSocket, buffer, size, 0, (struct sockaddr*)&clientAddress, sizeof(clientAddress));
- 关闭套接字:close(serverSocket);
2.客户端:
- 创建套接字:int clientSocket = socket(AF_INET, SOCK_DGRAM, 0);
- 发送和接收数据:sendto(clientSocket, buffer, size, 0, (struct sockaddr*)&serverAddress, sizeof(serverAddress)); recvfrom(clientSocket, buffer, size, 0, (struct sockaddr*)&serverAddress, sizeof(serverAddress));
- 关闭套接字:close(clientSocket);
Socket编程实例
以下是使用C语言通过Socket实现TCP和UDP通信的服务端和客户端通信程序的示例:
TCP通信示例:
服务端(server.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024int main() {int serverSocket, clientSocket;struct sockaddr_in serverAddr, clientAddr;socklen_t clientAddrLen;char buffer[BUFFER_SIZE];// 创建服务端套接字serverSocket = socket(AF_INET, SOCK_STREAM, 0);if (serverSocket == -1) {perror("Failed to create socket");exit(EXIT_FAILURE);}// 设置服务器地址信息memset(&serverAddr, 0, sizeof(serverAddr));serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(SERVER_PORT);serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);// 绑定套接字到指定地址和端口if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {perror("Failed to bind socket");exit(EXIT_FAILURE);}// 监听连接请求if (listen(serverSocket, 5) == -1) {perror("Failed to listen");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", SERVER_PORT);// 接受客户端连接clientAddrLen = sizeof(clientAddr);clientSocket = accept(serverSocket, (struct sockaddr *)&clientAddr, &clientAddrLen);if (clientSocket == -1) {perror("Failed to accept client connection");exit(EXIT_FAILURE);}printf("Client connected: %s\n", inet_ntoa(clientAddr.sin_addr));// 接收数据memset(buffer, 0, sizeof(buffer));if (recv(clientSocket, buffer, BUFFER_SIZE, 0) == -1) {perror("Failed to receive data");exit(EXIT_FAILURE);}printf("Client message: %s\n", buffer);// 发送响应strcpy(buffer, "Hello, Client!");if (send(clientSocket, buffer, strlen(buffer), 0) == -1) {perror("Failed to send data");exit(EXIT_FAILURE);}// 关闭套接字close(clientSocket);close(serverSocket);return 0;
}
客户端(client.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024int main() {int clientSocket;struct sockaddr_in serverAddr;char buffer[BUFFER_SIZE];// 创建客户端套接字clientSocket = socket(AF_INET, SOCK_STREAM, 0);if (clientSocket == -1) {perror("Failed to create socket");exit(EXIT_FAILURE);}// 设置服务器地址信息memset(&serverAddr, 0, sizeof(serverAddr));serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(SERVER_PORT);if (inet_pton(AF_INET, SERVER_IP, &(serverAddr.sin_addr)) <=0) {perror("Failed to set server IP");exit(EXIT_FAILURE);}// 连接到服务器if (connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {perror("Failed to connect to server");exit(EXIT_FAILURE);}printf("Connected to server %s:%d\n", SERVER_IP, SERVER_PORT);// 发送数据strcpy(buffer, "Hello, Server!");if (send(clientSocket, buffer, strlen(buffer), 0) == -1) {perror("Failed to send data");exit(EXIT_FAILURE);}// 接收响应memset(buffer, 0, sizeof(buffer));if (recv(clientSocket, buffer, BUFFER_SIZE, 0) == -1) {perror("Failed to receive data");exit(EXIT_FAILURE);}printf("Server response: %s\n", buffer);// 关闭套接字close(clientSocket);return 0;
}
UDP通信示例:
服务端(server.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024int main() {int serverSocket;struct sockaddr_in serverAddr, clientAddr;socklen_t clientAddrLen;char buffer[BUFFER_SIZE];// 创建服务端套接字serverSocket = socket(AF_INET, SOCK_DGRAM, 0);if (serverSocket == -1) {perror("Failed to create socket");exit(EXIT_FAILURE);}// 设置服务器地址信息memset(&serverAddr, 0, sizeof(serverAddr));serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(SERVER_PORT);serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);// 绑定套接字到指定地址和端口if (bind(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {perror("Failed to bind socket");exit(EXIT_FAILURE);}printf("Server listening on port %d...\n", SERVER_PORT);// 接收数据clientAddrLen = sizeof(clientAddr);memset(buffer, 0, sizeof(buffer));if (recvfrom(serverSocket, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&clientAddr, &clientAddrLen) == -1) {perror("Failed to receive data");exit(EXIT_FAILURE);}printf("Client message: %s\n", buffer);// 发送响应strcpy(buffer, "Hello, Client!");if (sendto(serverSocket, buffer, strlen(buffer), 0, (struct sockaddr *)&clientAddr, clientAddrLen) == -1) {perror("Failed to send data");exit(EXIT_FAILURE);}// 关闭套接字close(serverSocket);return 0;
}
客户端(client.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8080
#define BUFFER_SIZE 1024int main() {int clientSocket;struct sockaddr_in serverAddr;char buffer[BUFFER_SIZE];// 创建客户端套接字clientSocket = socket(AF_INET, SOCK_DGRAM, 0);if (clientSocket == -1) {perror("Failed to create socket");exit(EXIT_FAILURE);}// 设置服务器地址信息memset(&serverAddr, 0, sizeof(serverAddr));serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(SERVER_PORT);if (inet_pton(AF_INET, SERVER_IP, &(serverAddr.sin_addr)) <= 0) {perror("Failed to set server IP");exit(EXIT_FAILURE);}printf("Connected to server %s:%d\n", SERVER_IP, SERVER_PORT);// 发送数据strcpy(buffer, "Hello, Server!");if (sendto(clientSocket, buffer, strlen(buffer), 0, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) == -1) {perror("Failed to send data");exit(EXIT_FAILURE);}// 接收响应memset(buffer, 0, sizeof(buffer));if (recvfrom(clientSocket, buffer, BUFFER_SIZE, 0, NULL, NULL) == -1) {perror("Failed to receive data");exit(EXIT_FAILURE);}printf("Server response: %s\n", buffer);// 关闭套接字close(clientSocket);return 0;
}