socket 编程基础(网络编程)
socket 简介
定义与用途
-
套接字(socket)是一种在Linux下的进程间通信机制(socket IPC)
-
可以用于不同主机上的应用程序之间的通信(网络通信),也可以用于同一台主机上的不同应用程序之间的通信
通信模式
-
通常采用客户端<—>服务器的模式进行通信
-
多个客户端可以同时连接到服务器,与其进行数据交互
开发接口
- 内核向应用层提供了socket接口,开发人员只需调用该接口来开发应用程序
抽象层描述
-
Socket是应用层与TCP/IP协议通信的中间软件抽象层,是一组接口
-
在设计模式中,socket是一个门面模式(Facade Pattern),它将复杂的TCP/IP协议隐藏在socket接口后面,为用户提供简化的接口
简化开发
-
用户无需深入理解TCP/UDP等复杂的TCP/IP协议,socket已经封装好了这些协议
-
按照socket接口的规定编程即可,程序自然遵循TCP/UDP标准
广泛应用与移植性
-
当前网络中的主流程序设计都使用socket进行编程,因为它简单易用且是一个标准(BSD socket)
-
基于socket接口编写的应用程序可以方便地移植到任何实现BSD socket标准的平台上,如LwIP、Windows、RT-Thread等
socket 编程接口介绍
使用 socket 接口需要在我们的应用程序代码中包含两个头文件
-
#include <sys/types.h> /* See NOTES */
-
#include <sys/socket.h>
socket()函数
-
Socket()函数原型
- #include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
- #include <sys/types.h> /* See NOTES */
int socket(int domain, int type, int protocol);
- domain
- 指定通信域,选择用于通信的协议族
-
- 协议族描述
- 常见值
- AF_INET:用于 TCP/IP 协议
- AF_INET6:支持 IPv6 时使用
- type
- 指定套接字的类型
- 套接字类型描述
- 常见值
- SOCK_STREAM:流式套接字,默认协议为 TCP
- SOCK_DGRAM:数据报套接字,默认协议为 UDP
- protocol
- 通常设置为 0,表示选择默认协议
- 特殊情况
- 在同一域和套接字类型支持多个协议时,可使用此参数选择特定协议
- 返回值:成功时返回 socket 描述符,失败时返回 -1 并设置 errno 变量
-
功能
- 类似于 open() 函数,用于创建一个网络通信端点,成功则返回一个网络文件描述符,称为 socket 描述符
-
资源释放
- 不再需要时,调用 close() 函数关闭套接字,释放资源
-
使用示例
- int socket_fd = socket(AF_INET, SOCK_STREAM, 0);//打开套接字
if (0 > socket_fd) {
perror(“socket error”);
exit(-1);
}
- int socket_fd = socket(AF_INET, SOCK_STREAM, 0);//打开套接字
…
…
close(socket_fd); //关闭套接字
bind()函数
-
将一个 IP 地址或端口号与一个套接字进行绑定,即将套接字与地址关联
-
bind()函数原型
-
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
sockfd
- 指定要绑定的套接字描述符
-
addr
-
指向一个 struct sockaddr 类型变量的指针,包含要绑定的地址信息
-
struct sockaddr 结构体:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}- 通用 socket 地址结构体,不友好,用户无法直接赋值
-
sockaddr_in 和 sockaddr 是并列的结构(占
用的空间是一样的) -
struct sockaddr_in 结构体:
struct sockaddr_in {
sa_family_t sin_family; /* 协议族 /
in_port_t sin_port; / 端口号 /
struct in_addr sin_addr; / IP 地址 */
unsigned char sin_zero[8];
};- 更友好的结构体,使用时需类型转换
-
-
-
addrlen
- 指定 addr 所指向的结构体的字节长度
-
成功返回 0,失败返回 -1 并设置 errno 以提示错误原因
-
-
使用场景
-
服务器端:通常将服务器的套接字绑定到一个众所周知的地址(IP 地址和端口号),以便客户端提前知道
-
客户端:可以让系统选一个默认地址,不需要显式调用 bind()
-
-
使用示例
- struct sockaddr_in socket_addr;
memset(&socket_addr, 0x0, sizeof(socket_addr)); //清零
- struct sockaddr_in socket_addr;
//填充变量
socket_addr.sin_family = AF_INET;
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(5555);
//将地址与套接字进行关联、绑定
bind(socket_fd, (struct sockaddr *)&socket_addr, sizeof(socket_addr));
-
注意事项
-
大小端问题:使用 htons 和 htonl 宏定义来避免大小端问题,需包含头文件 <netinet/in.h>
-
可选调用:bind() 函数不是必须调用的,依赖内核的自动选址机制在客户端应用程序中常见
-
-
bind() 函数在网络编程中用于将套接字与特定地址绑定,确保服务器端有固定的通信地址,而客户端则可以依赖系统自动分配地址。
listen()函数
-
让服务器进程进入监听状态,等待客户端的连接请求
- 通过设置等待连接队列的大小,确保服务器能够有序处理客户端的连接请求,同时避免因请求过多导致系统资源耗尽
-
listen()函数原型
-
int listen(int sockfd, int backlog);
-
sockfd
- 指定要进入监听状态的套接字描述符
-
backlog
-
限制内核维护的等待连接队列的大小,防止队列无限增长
-
描述 sockfd 的等待连接队列能够达到的最大值
-
当队列满时,新的连接请求会被丢弃,客户端可能会收到连接失败的错误
-
-
成功返回 0,失败返回 -1
-
-
调用顺序
- 一般在 bind() 函数之后,accept() 函数之前调用
-
使用限制
- 只能在服务器进程中使用:不能在已经连接的套接字(即已经成功执行 connect() 或由 accept() 返回的套接字)上执行 listen()
-
处理连接请求
-
队列机制:内核会在进程空间维护一个队列,按照先来后到的顺序处理连接请求
-
队列上限:backlog 参数设置队列的最大长度,超过此值的请求会被丢弃
-
-
错误处理
- 队列满:当队列满时,客户端的连接请求会被丢弃,客户端可能会收到错误
accept()函数
-
服务器进入监听状态
-
服务器调用 listen() 函数后进入监听状态,等待客户端连接请求
-
使用 accept() 函数获取客户端的连接请求并建立连接
-
-
accept() 函数原型
-
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
-
addr: 传出参数,用于返回已连接客户端的 IP 地址和端口号等信息
-
addrlen: 设置为 addr 所指向对象的字节长度
-
如果对客户端的 IP 地址和端口号不感兴趣,可以将 addr 和 addrlen 置为空指针 NULL
-
-
服务器处理流程
-
步骤 1: 调用 socket() 函数打开套接字
-
步骤 2: 调用 bind() 函数将套接字与一个端口号和 IP 地址绑定
-
步骤 3: 调用 listen() 函数进入监听状态,等待客户端连接请求
-
步骤 4: 调用 accept() 函数处理到来的连接请求
-
-
accept() 函数的特性与行为
-
accept() 通常只用于服务器应用程序
-
如果调用 accept() 时没有客户端连接请求,函数会进入阻塞状态,直到有请求到达
-
当客户端连接请求到达时,accept() 会建立连接并返回一个新的套接字
-
这个新套接字与 socket() 函数返回的服务器套接字不同
-
服务器通过新套接字与客户端进行数据交互(如发送或接收数据)
-
-
accept() 函数的关键点
-
accept() 会创建一个新套接字,与客户端建立连接
-
这个新套接字代表服务器与客户端的一个连接
-
如果 accept() 出错,会返回 -1,并设置 errno 指示错误原因
-
connect()函数
-
用途
-
该函数用于客户端应用程序中
-
客户端调用 connect() 函数将套接字 sockfd 与远程服务器进行连接
-
-
connect() 函数原型
-
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
-
sockfd: 套接字描述符
-
addr: 指定待连接服务器的 IP 地址和端口号等信息
-
addrlen: 指定 addr 所指向的 struct sockaddr 对象的字节大小
-
成功则返回 0,失败返回-1,并设置 errno 以指示错误原因
-
-
TCP 和 UDP 的不同处理
-
TCP连接
- 调用 connect() 函数将触发 TCP 连接的握手过程,并最终建立一个 TCP 连接
-
UDP协议
- 调用 connect() 函数只是在 sockfd 中记录服务器的 IP 地址与端口号,不发送任何数据
-
发送和接收函数
-
通过套接字描述符收发数据
-
客户端使用 socket() 返回的套接字描述符
-
服务器使用 accept() 返回的套接字描述符
-
可以调用 read() 或 recv() 函数读取网络数据,调用 write() 或 send() 函数发送数据
-
-
read() 函数
-
从文件描述符读取指定字节大小的数据并放入缓冲区
-
成功返回读取的字节数;返回值小于指定字节数并不意味着错误,可能是因为文件接近结尾或其他原因
-
出错返回 -1 并设置 errno;如果到达文件末尾,返回 0
-
套接字描述符也是文件描述符,读取网络数据时参数 fd 就是套接字描述符
-
-
recv() 函数
-
客户端和服务器均可调用,用于读取网络数据
-
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
-
sockfd: 套接字描述符
-
buf: 数据接收缓冲区
-
len: 读取数据的字节大小
-
flags: 控制如何接收数据的标志
-
recv() 与 read() 类似,但可以通过 flags 控制接收行为
-
flags 通常设置为 0;可以设置 MSG_PEEK 查看但不取走数据,设置 MSG_WAITALL 等待全部数据返回
-
recv 函数标志描述
-
-
成功返回实际读取的字节数;发送方已结束传输时,返回 0
-
-
对于 SOCK_STREAM 类型,接收的数据可能少于指定字节大小;MSG_WAITALL 会阻止这种行为
-
-
write() 函数
-
向套接字描述符写入数据
-
成功返回写入的字节数,失败返回 -1 并设置 errno
-
-
send() 函数
-
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
-
与 write 类似,但可以通过 flags 改变传输数据的处理方式
-
成功返回并不表示对端进程一定接收到数据;表示数据已发送到网络驱动程序
-
close()关闭套接字
- 当不再需要套接字描述符时,可调用 close()函数来关闭套接字,释放相应的资源
IP 地址格式转换函数
转换需求
-
易于阅读的是点分十进制的 IP 地址形式,如 192.168.1.110、192.168.1.50
-
这些点分十进制形式实际上是字符串
-
计算机理解的是二进制形式的 IP 地址
inet_aton、inet_addr、inet_ntoa 函数
- 这些函数可将一个 IP 地址在点分十进制表示形式和二进制表示形式之间进行转换,这些函数已经废弃了,基本不用这些函数了,但是在一些旧的代码中可能还会看到这些函数
inet_ntop、inet_pton 函数
-
支持 IPv6 地址
-
这些函数不仅支持 IPv4 地址,还支持 IPv6 地址
-
它们可以将二进制 IPv4 或 IPv6 地址转换成点分十进制表示的字符串形式,或者将点分十进制表示的字符串形式转换成二进制 IPv4 或 IPv6 地址
-
-
包含头文件
- 使用 inet_ntop() 和 inet_pton() 函数只需包含 <arpa/inet.h> 头文件
-
inet_pton() 函数
-
将点分十进制表示的字符串形式转换成二进制 IPv4 或 IPv6 地址
-
int inet_pton(int af, const char *src, void *dst);
-
af: 地址族
-
AF_INET 表示待转换的是 IPv4 地址
-
AF_INET6 表示待转换的是 IPv6 地址
-
-
src: 指向待转换的字符串
-
dst: 指向存储转换后地址的结构体对象
-
如果 af 为 AF_INET,则 dst 应指向 struct in_addr 结构体对象
-
如果 af 为 AF_INET6,则 dst 应指向 struct in6_addr 结构体对象
-
-
返回值
-
成功返回 1(表示已成功转换)
-
如果 src 不包含有效的地址字符串,返回 0
-
如果 af 不包含有效的地址族,返回 -1 并设置 errno 为 EAFNOSUPPORT
-
-
-
使用示例
- #include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
- #include <stdio.h>
-
#define IPV4_ADDR “192.168.1.222”
int main(void)
{
struct in_addr addr;
inet_pton(AF_INET, IPV4_ADDR, &addr);
printf("ip addr: 0x%x\n", addr.s_addr);
exit(0);
}
- 测试结果
-
-
inet_ntop()函数
-
将二进制 IP 地址转换为点分十进制形式的字符串
-
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
-
af: 地址族
- 与 inet_pton() 函数的 af 参数意义相同
-
src: 指向二进制 IP 地址的结构体对象
- 应依据 af 参数指向 struct in_addr 或 struct in6_addr 结构体对象
-
dst: 指向存储转换后字符串的缓冲区
-
size: 指定缓冲区的大小
-
返回值
-
成功时返回 dst 指针
-
如果 size 太小,返回 NULL 并设置 errno 为 ENOSPC
-
-
-
使用示例
- #include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
- #include <stdio.h>
-
int main(void)
{
struct in_addr addr;
char buf[20] = {0};
addr.s_addr = 0xde01a8c0;
inet_ntop(AF_INET, &addr, buf, sizeof(buf));
printf("ip addr: %s\n", buf);
exit(0);
}
- 测试结果
-
socket 编程实战
编写服务器程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //端口号不能发生冲突,不常用的端口号通常大于5000
int main(void)
{
struct sockaddr_in server_addr = {0}; // 服务器地址结构体,初始化为0
struct sockaddr_in client_addr = {0}; // 客户端地址结构体,初始化为0
char ip_str[20] = {0}; // 用于存储客户端IP地址的字符串
int sockfd, connfd; // 套接字描述符
int addrlen = sizeof(client_addr); // 客户端地址结构体的大小
char recvbuf[512]; // 接收缓冲区
int ret; // 用于存储系统调用返回值
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 将套接字与指定端口号进行绑定 */
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网络接口
server_addr.sin_port = htons(SERVER_PORT); // 设置服务器端口号
ret = bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 绑定套接字
if (0 > ret) {
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 使服务器进入监听状态 */
ret = listen(sockfd, 50);
if (0 > ret) {
perror("listen error");
close(sockfd);
exit(EXIT_FAILURE);
}
/* 阻塞等待客户端连接 */
connfd = accept(sockfd, (struct sockaddr *)&client_addr, &addrlen);
if (0 > connfd) {
perror("accept error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("有客户端接入...\n");
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
printf("客户端主机的IP地址: %s\n", ip_str);
printf("客户端进程的端口号: %d\n", client_addr.sin_port);
/* 接收客户端发送过来的数据 */
for ( ; ; ) {
// 接收缓冲区清零
memset(recvbuf, 0x0, sizeof(recvbuf));
// 读数据
ret = recv(connfd, recvbuf, sizeof(recvbuf), 0);
if(0 >= ret) {
perror("recv error");
close(connfd);
break;
}
// 将读取到的数据以字符串形式打印出来
printf("from client: %s\n", recvbuf);
// 如果读取到"exit"则关闭套接字退出程序
if (0 == strncmp("exit", recvbuf, 4)) {
printf("server exit...\n");
close(connfd);
break;
}
}
/* 关闭套接字 */
close(sockfd);
exit(EXIT_SUCCESS);
}
-
程序的流程
-
调用 socket()函数打开套接字,得到套接字描述符
-
调用 bind()函数将套接字与 IP 地址、端口号进行绑定
-
调用 listen()函数让服务器进程进入监听状态
-
调用 accept()函数获取客户端的连接请求并建立连接
-
调用 read/recv、write/send 与客户端进行通信
-
调用 close()关闭套接字
-
-
SERVER_PORT 宏指定了本服务器绑定的端口号,这里我们将端口号设置为 8888,端口不能与其它服务器的端口号发生冲突,不常用的端口号通常大于 5000
编写客户端程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define SERVER_PORT 8888 //服务器的端口号
#define SERVER_IP "192.168.2.192" //服务器的IP地址
int main(void)
{
struct sockaddr_in server_addr = {0}; // 服务器地址结构体,初始化为0
char buf[512]; // 数据缓冲区
int sockfd; // 套接字描述符
int ret; // 用于存储系统调用返回值
/* 打开套接字,得到套接字描述符 */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd) {
perror("socket error");
exit(EXIT_FAILURE);
}
/* 调用connect连接远端服务器 */
server_addr.sin_family = AF_INET; // 设置地址族为IPv4
server_addr.sin_port = htons(SERVER_PORT); //端口号
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);//IP地址
ret = connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if (0 > ret) {
perror("connect error");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("服务器连接成功...\n\n");
/* 向服务器发送数据 */
for ( ; ; ) {
// 清理缓冲区
memset(buf, 0x0, sizeof(buf));
// 接收用户输入的字符串数据
printf("Please enter a string: ");
fgets(buf, sizeof(buf), stdin);
// 将用户输入的数据发送给服务器
ret = send(sockfd, buf, strlen(buf), 0);
if(0 > ret){
perror("send error");
break;
}
//输入了"exit",退出循环
if(0 == strncmp(buf, "exit", 4))
break;
}
close(sockfd);
exit(EXIT_SUCCESS);
}
-
连接上面所实现的服务器,连接成功之后向服务器发送数据,发送的数据由用户输入
-
程序的流程
-
调用 socket()函数打开套接字,得到套接字描述符
-
调用connect连接远端服务器
-
设置地址族为IPv4
-
设置服务器的端口号
-
将服务器的IP地址转换为二进制形式
-
-
向服务器发送数据
-
清理缓冲区
-
接收用户输入的字符串数据
-
将用户输入的数据发送给服务器
-
输入了"exit",退出循环
-
-
调用 close()关闭套接字
-
-
SERVER_IP 和 SERVER_PORT 指的是服务器的 IP 地址和端口号,服务
器的 IP 地址根据实际情况进行设置,服务器应用程序示例代码中我们绑定的端口号为 8888,所以在客户端应用程序中我们也需要指定 SERVER_PORT 为 8888
编译测试
-
以将服务器程序运行在开发板上,而将客户端应用程序运行在 Ubuntu 系统为例
-
流程
-
编译服务器应用程序和客户端应用程序
-
用gcc编译client
- gcc -o client socket_client.c
-
用poky编译server
- ${CC} -o server socket_server.c
-
-
将服务器执行文件拷贝到开发板
-
先执行服务器应用程序
-
执行客户端应用程序
-
服务器监测到客户端连接
-
输入字符串信息
-
服务器接收到客户端发送的信息
-