一、前言
- 本文系统使用: Ubuntu 20.04.6 LTS
- Linux 内核版本为:5.15.0-136-generic
- 嵌入式Linux学习交流群:1005210698
- 更多免费资料可加群获取
二、概述
什么是Socket
- Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。
- Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。
- 网络的 Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
- 常用的Socket类型有两种:流式Socket (SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。
- 流式是一种面向连接的Socket,针对于面向连接的TCP服务应用。
- 数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。
TCP/UDP通信交互图
-
面向连接的 TCP 流模式
-
UDP用户数据包模式
三、函数介绍
1.Socket建立
- 为了建立
Socket
,程序可以调用Socket
函数,该函数返回一个类似于文件描述符的句柄。
函数原型:
int socket(int domain, int type, int protocol);
参数说明:
- domain :说明我们网络程序所在的主机采用的通讯协族(
AF_UNIX
和AF_INET
等)AF_UNIX
只能够用于单一的Unix
系统进程间通信AF_INET
是针对Internet
的,因而可以允许在远程
- type :是网络程序所采用的通讯协议(
SOCK_STREAM
,SOCK_DGRAM
等)SOCK_STREAM
表明我们用的是TCP
协议,这样会提供按顺序的、可靠、双向、面向连接的比特流.SOCK_DGRAM
表明我们用的是UDP
协议,这样只会提供定长的、不可靠,无连接的通信
- protocol:由于指定了
type
,所以这个地方一般只要用0
来代替就可以了socket
为网络通讯做基本的准备
Socket
描述符是一个指向内部数据结构的指针,它指向描述符表入口。- 调用
Socket
函数时,socket
执行体将建立一个Socket
,实际上”建立一个Socket
“意味着为一个Socket
数据结构分配存储空间Socket
执行体为你管理描述符表。
2.Socket配置
- 面向连接的
socket
客户端通过调用connect
函数在socket
数据结构中保存本地和远端信息 - 无连接
socket
的客户端和服务端以及面向连接socket
的服务端通过调用bind
函数来配置本地信息 - 自动获得本机IP地址和随机获取一个没有被占用的端口号
函数原型:
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
参数说明:
- sockfd:是由
socket
函数调用返回的文件描述符 - my_addr:是一个指向包含有本机IP地址及端口号等信息的
sockaddr
类型的指针 - addrlen:是
sockaddr
结构的长度
涉及结构体:
struct sockaddr{
unisgned short as_family; /* 地址族, AF_xxx */
char sa_data[14]; /* 14 字节的协议地址 */
};
- 不过由于系统的兼容性,一般不用这个头文件,而使用另外一个结构
(struct sockaddr_in)
来代替
struct sockaddr_in{
unsigned short sin_family; /* 地址族 */
unsigned short int sin_port; /* 端口号 */
struct in_addr sin_addr; /* IP地址 */
unsigned char sin_zero[8]; /* 填充0 以保持与struct sockaddr同样大小 */
}
如果使用 Internet
所以 sin_family
一般为 AF_INET
。
sin_addr
设置为INADDR_ANY
表示可以和任何的主机通信sin_port
是要监听的端口号sin_zero
用来将sockaddr_in
结构填充到与struct sockaddr
同样的长度,可以用bzero()
或memset()
函数将其置为零
- 计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。
- 在
Internet
上数据以高位字节优先顺序在网络上传输- 在内部是以低位字节优先方式存储数据的机器
- 在
Internet
上传输数据时就需要进行转换,否则就会出现数据不一致
3.连接建立
- 使 socket 处于被动的监听模式,并为该 socket 建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们
函数原型:
int listen(int sockfd, int backlog);
参数说明:
- sockfd:是
bind
后的文件描述符. - backlog:
- 指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待
accept()
它们 - 对队列中等待服务的请求的数目进行了限制,大多数系统缺省值为20
- 如果一个服务请求到来时,输入队列已满,该
socket
将拒绝连接请求,客户将收到一个出错信息
- 指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待
- 建立好输入队列后,服务器睡眠并等待客户的连接请求
函数原型:
int accept(int fd, struct sockaddr *restrict addr, socklen_t *restrict addrlen);
参数说明:
- sockfd:是
listen
后的文件描述符 - addr:是一个指向
sockaddr_in
变量的指针,用来存放提出连接请求服务的主机的信息(某台主机从某个端口发出该请求) - addrlen:通常为一个指向值为
sizeof(struct sockaddr_in)
的整型指针变量 - 成功时返回最后的服务器端的文件描述符,此时服务器端可以向该描述符写信息了
addr
、addrlen
是用来给客户端的程序填写的,服务器端只要传递指针就可以了accept
调用时,服务器端的程序会一直阻塞到有一个 客户程序发出了连接- 当
accept
函数监视的socket
收到连接请求时,socket
执行体将建立一个新的socket
,执行体将这个新 socket和请求连接进程的地址联系起来,收到服务请求的初始
socket
仍可以继续在以前的socket
上监听,同时可以在新的socket
描述符上进行数据传输操作
- 客户程序与远端服务器建立一个连接
函数原型:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
参数说明:
- sockfd:是由
socket
函数调用返回的文件描述符 - serv_addr:储存了服务器端的连接信息,其中
sin_add
是服务端的地址 - addrlen:
serv_addr
的长度
[!info]+ 信息
- 进行客户端程序设计无须调用
bind()
,因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心socket
执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候到端口connect
函数启动和远端主机的直接连接- 只有面向连接的客户程序使用
socket
时才需要将此socket
与远端主机相连,无连接协议从不建立直接连接- 面向连接的服务器也从不启动一个连接,它只是被动的在协议端口监听客户的请求
4.数据传输
- 发送数据
函数原型:
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数说明:
- sockfd:指定发送端套接字描述符
- buf:是一个指向要发送数据的指针
- len:是以字节为单位的数据的长度
- flags:一般情况下置为 0
函数原型:
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:表示目地机的IP地址和端口号信息
- addrlen:常常被赋值为 sizeof (struct sockaddr)
[!help]+ 说明
sendto
和send
相似,区别在于sendto
允许在无连接的套接字上指定一个目标地址
- 接收数据
函数原型:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数说明:
- sockfd:指定接收端套接字描述符
- buf:指明一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据
- len:指明 buf 的长度
- flags:一般情况下置为 0
函数原型:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
- sockfd:指定接收端套接字描述符
- buf:指明一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据
- len:指明 buf 的长度
- flags:一般情况下置为 0
- src_addr:是一个 struct sockaddr 类型的变量,该变量保存源机的 IP 地 址及端口号
- addrlen 常置为 sizeof (struct sockadd)
recv
和recvfrom
相似,recvfrom
通常用于无连接套接字,因为此函数可以获得发送者的地址
5.结束传输
- 当所有的数据操作结束以后,可以调用
close()
函数来释放该socket
,从而停止在该socket
上的任何数据操作 - 也可以调用
shutdown()
函数来关闭该socket
- 该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行
- 如可以关闭某
socket
的写操作而允许继续在该socket
上接受数据,直至读入所有数据
函数原型:
int shutdown(int sockfd, int how);
参数说明:
- sockfd:需要关闭的socket的描述符
- how:
- 0:不允许继续接收数据
- 1:不允许继续发送数据
- 2:不允许继续发送和接收数据
- 均为允许则调用 close ()
6.数据转换
1.字节转换函数
- 在网络上面有着许多类型的机器,这些机器在表示数据的字节顺序是不同的,比如i386芯片是低字节在内存地址的低端
- 高字节在高端,而 alpha 芯片却相反,为了统一起来,在 Linux 下面,有专门的字节转换函数
unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unisgned short int hostshort);
unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);
- 在这四个转换函数中,h 代表 host,n 代表 network,s 代表short,l 代表long
2.IP和域名的转换
struct hostent *gethostbyname(const char *hostname);
struct hostent *gethostbyaddr(const char *addr,int len,int type);
- gethostbyname:可以将机器名(如 linux.yessun.com )转换为一个结构指针,在这个结构里面储存了域名的信息
- gethostbyaddr:可以将一个32位的IP地址(C0A80001)转换为结构指针
- hostent 结构体:
struct hostent{
char *h_name; /* 主机的官方域名 */
char **h_aliases; /* 别名列表(以 NULL 结尾) */
int h_addrtype; /* 返回的地址类型,在Internet环境下为AF-INET */
int h_length; /* 地址的字节长度 */
char **h_addr_list; /* 一个以0结尾的数组,包含该主机的所有地址*/
#ifdef __USE_MISC
# define h_addr h_addr_list[0] /* 兼容旧版本的快捷访问方式 */
#endif
};
3.字符串的IP和32位的IP转换
- 在网络上面我们用的IP都是数字加点 (192.168.0.1) 构成的,而在 struct in_addr 结构中用的是32位的IP
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
- inet_aton:将点分十进制 IPv4 地址(如
192.168.1.1
)转换为网络字节序的二进制格式,并存入inp
指向的结构体 - inet_ntoa:将网络字节序的二进制 IPv4 地址转换为点分十进制字符串
四、代码编写
TCP编程
服务端
- 首先调用 socket 函数创建一个 Socket ,然后调用 bind 函数将其与本机地址以及一个本地端口号绑定,然后调用 listen 在相应的 socket 上监听,当 accpet 接收到一个连接服务请求时,将生成一个新的 socket
- 服务器显示该客户机的IP地址,并通过新的 socket 向客户端发送字符串"您好,您已接通!"
- 循环接收来之客户机的消息
/******************************************************************
* 个人博客:https://blog.csdn.net/2302_80277720?type=blog
* 嵌入式Linux学习交流群:1005210698
* 欢迎各位大佬和萌新来加入交流学习
* Change Logs:
* Date Author Notes
* 2025-03-02 喝呜昂黄 first version
******************************************************************/
#include "stdio.h"
#include "sys/socket.h"
#include <arpa/inet.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#define SERVER_PORT 8888
#define BACKLOG 10
/* socket
* bind
* listen
* accept
* send/recv
*/
int main(int argc, char **argv){
int ret;
int server_socket, client_socket; // 监听套接字和客户端连接套接字
struct sockaddr_in server_addr, client_addr; // 服务器和客户端地址结构体
socklen_t sin_size; // 客户端地址结构体长度
int recv_len; // 接收数据的长度
const char *welcome = "您好,您已接通!";
uint8_t recv_buf[1000]; // 接收数据缓冲区
int client_num = 0; // 客户端连接计数器
pid_t pid; // 进程ID
// 创建监听套接字
// AF_INET: IPv4协议族, SOCK_STREAM: TCP流式套接字, 0: 默认协议(TCP)
server_socket = socket(AF_INET, SOCK_STREAM, 0);
if(server_socket == -1){
printf("socket error\n");
exit(EXIT_FAILURE);
}
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // 设置协议族
server_addr.sin_port = htons(SERVER_PORT); // 端口号(主机字节序转网络字节序)
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有本地IP地址
// sin_zero: 填充字段(仅用于兼容旧版sockaddr结构体,必须置零)
memset(server_addr.sin_zero, 0, 8);
// 绑定地址到套接字
ret = bind(server_socket, (const struct sockaddr *)&server_addr, sizeof(server_addr));
if(ret == -1){
perror("bind error\n");
exit(EXIT_FAILURE);
}
// 开始监听连接
ret = listen(server_socket, BACKLOG);
if(ret == -1){
perror("listen error\n");
exit(EXIT_FAILURE);
}
// 忽略SIGCHLD信号,系统自动回收僵尸进程
signal(SIGCHLD, SIG_IGN);
while (1) {
// 获得连接请求,并建立连接
// accept()从等待队列中取出一个连接,返回新的套接字用于通信
sin_size = sizeof(struct sockaddr_in);
client_socket = accept(server_socket, (struct sockaddr *)&client_addr , &sin_size);
if(client_socket == -1){
perror("accept error");
continue;
}
client_num++;
printf("接受来自客户端 %d 的连接:%s %d\n",
client_num,
inet_ntoa(client_addr.sin_addr), // 转换IP地址为点分十进制字符串
ntohs(client_addr.sin_port)); // 网络字节序转主机字节序
// 创建子进程处理客户端
pid = fork();
if(pid < 0){
perror("fork error");
close(client_socket);
continue;
}
if(pid > 0){
close(client_socket);
continue;
}
// 子进程不需要监听套接字
close(server_socket);
// 向客户端发送欢迎消息
ret = send(client_socket, welcome, strlen(welcome), 0);
if(ret == -1){
perror("send error");
close(client_socket);
exit(EXIT_FAILURE);
}
// 循环接收客户端消息
while (1) {
recv_len = recv(client_socket, recv_buf, sizeof(recv_buf), 0);
if(recv_len <= 0){
printf("客户端 %d(%s:%d)已断开连接\n",
client_num,
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
close(client_socket);
exit(EXIT_FAILURE);
}
else {
recv_buf[recv_len] = '\0';
printf("来自客户端 %d(%s %d)的发送的信息:%s\n",
client_num,
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
recv_buf);
}
}
close(client_socket);
exit(0);
}
close(server_socket);
return 0;
}
客户端
- 首先通过服务器域名获得服务器的IP地址,然后创建一个 socket ,调用 connect 函数与服务器建立连接,连接成功之后接收从服务器发送过来的数据,然后循环等待键盘输入数据,按下回车发送到服务端
/******************************************************************
* 个人博客:https://blog.csdn.net/2302_80277720?type=blog
* 嵌入式Linux学习交流群:1005210698
* 欢迎各位大佬和萌新来加入交流学习
* Change Logs:
* Date Author Notes
* 2025-03-02 喝呜昂黄 first version
******************************************************************/
#include <arpa/inet.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
/* socket
* connect
* send/recv
*/
// 服务器端口号(需与服务器端一致)
#define SERVER_PORT 8888
int main(int argc, char **argv){
int client_socket; // 客户端套接字描述符
struct sockaddr_in server_addr; // 服务器地址结构体
int ret;
char send_buf[1000];
uint8_t recv_buf[1000];
size_t send_len;
ssize_t recv_len;
if(argc != 2){
printf("Usage: \n");
printf("%s <server_ip>\n", argv[0]);
exit(-1);
}
// 创建TCP套接字
// AF_INET: IPv4协议, SOCK_STREAM: TCP流式套接字
client_socket = socket(AF_INET, SOCK_STREAM, 0);
if(client_socket == -1){
perror("Socket error");
exit(1);
}
// 初始化服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // 设置协议族
ret = inet_aton(argv[1], &server_addr.sin_addr); // 将点分十进制 IPv4 地址(如 `192.168.1.1`)转换为网络字节序的二进制格式
if(ret == 0){
perror("invalid server ip");
exit(1);
}
server_addr.sin_port = htons(SERVER_PORT); // 端口号转网络字节序
memset(server_addr.sin_zero, 0, 8); // sin_zero: 填充字段(必须置零,兼容旧版API)
// 连接到服务器
ret = connect(client_socket, (const struct sockaddr *)&server_addr, sizeof(server_addr));
if(ret == -1){
perror("Connect error");
exit(1);
}
// 接收服务器消息
recv_len = recv(client_socket, recv_buf, 100, 0);
if(ret == -1){
perror("recv error");
exit(1);
}
recv_buf[recv_len] = '\0';
printf("Received: %s\n", recv_buf);
// 发送数据到服务器
while (1) {
if(fgets(send_buf, sizeof(send_buf), stdin) == NULL){
printf("输入结束(EOF),退出程序\n");
break;
}
send_len = send(client_socket, send_buf, strlen(send_buf), 0);
if(send_len <= 0){
printf("服务器已断开连接\n");
close(client_socket);
exit(1);
}
printf("发送成功\n");
}
printf("客户端已退出\n");
close(client_socket);
return 0;
}
UDP编写
服务端
/******************************************************************
* 个人博客:https://blog.csdn.net/2302_80277720?type=blog
* 嵌入式Linux学习交流群:1005210698
* 欢迎各位大佬和萌新来加入交流学习
* Change Logs:
* Date Author Notes
* 2025-03-02 喝呜昂黄 first version
******************************************************************/
#include <stdlib.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#define SERVER_PORT 8888
int main(int argc, char **argv){
int ret;
int server_socket; // 监听套接字套接字
struct sockaddr_in server_addr, client_addr; // 服务器和客户端地址结构体
socklen_t sin_size; // 客户端地址结构体长度
int recv_len; // 接收数据的长度
uint8_t recv_buf[1000]; // 接收数据缓冲区
// 创建监听套接字
server_socket = socket(AF_INET, SOCK_DGRAM, 0);
if(server_socket == -1){
perror("socket error");
exit(1);
}
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// sin_zero: 填充字段(仅用于兼容旧版sockaddr结构体,必须置零)
memset(server_addr.sin_zero, 0, 8);
// 绑定地址到套接字
sin_size = sizeof(server_addr);
ret = bind(server_socket, (const struct sockaddr *)&server_addr, sin_size);
if(ret == -1){
perror("bind error");
exit(1);
}
// 循环接收客户端消息
while (1) {
recv_len = recvfrom(server_socket, recv_buf,
sizeof(recv_buf), 0,
(struct sockaddr *)&client_addr, &sin_size);
if(recv_len > 0) {
recv_buf[recv_len] = '\0';
printf("来自客户端 %s %d)的发送的信息:%s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
recv_buf);
}
}
close(server_socket);
return 0;
}
客户端
/******************************************************************
* 个人博客:https://blog.csdn.net/2302_80277720?type=blog
* 嵌入式Linux学习交流群:1005210698
* 欢迎各位大佬和萌新来加入交流学习
* Change Logs:
* Date Author Notes
* 2025-03-02 喝呜昂黄 first version
******************************************************************/
#include <arpa/inet.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
// 服务器端口号(需与服务器端一致)
#define SERVER_PORT 8888
int main(int argc, char **argv){
int client_socket; // 客户端套接字描述符
struct sockaddr_in server_addr; // 服务器地址结构体
int ret;
char send_buf[1000];
uint8_t recv_buf[1000];
size_t send_len;
ssize_t recv_len;
socklen_t sin_size;
if(argc != 2){
printf("Usage: \n");
printf("%s <server_ip>\n", argv[0]);
exit(-1);
}
// 创建TCP套接字
// AF_INET: IPv4协议, SOCK_STREAM: TCP流式套接字
client_socket = socket(AF_INET, SOCK_DGRAM, 0);
if(client_socket == -1){
perror("Socket error");
exit(1);
}
// 初始化服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
ret = inet_aton(argv[1], &server_addr.sin_addr);
if(ret == 0){
perror("invalid server ip");
exit(1);
}
server_addr.sin_port = htons(SERVER_PORT);
memset(server_addr.sin_zero, 0, 8);
sin_size = sizeof(server_addr);
// 发送数据到服务器
while (1) {
if(fgets(send_buf, sizeof(send_buf), stdin) == NULL){
printf("输入结束(EOF),退出程序\n");
break;
}
send_len = sendto(client_socket, send_buf,
strlen(send_buf), 0,
(struct sockaddr *)&server_addr, sin_size);
if(send_len <= 0){
printf("服务器已断开连接\n");
close(client_socket);
exit(1);
}
printf("发送成功\n");
}
printf("客户端已退出\n");
close(client_socket);
return 0;
}
五、参考
本文参考《Linux网络编程入门 (转载)》、《Linux下Socket编程》