一、Socket简介(套接字)
TCP/IP 五层网络模型的应用层编程接口称为Socket API, Socket( 套接字 ) ,它是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。 一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议>网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议>网络协议栈是应用程序通过网络协议>网络协议进行网络通信的接口
Socket 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概
念。
为什么需要Socket
1. 普通的 I/O 操作
打开文件->读/ 写操作->关闭文件
2. 网络通信
TCP/IP协议被集成到操作系统的内核中,引入了新型的 “I/O” 操作.
既然是文件,那么我们可以使用文件描述符引用套接字。与管道类似,Linux 系统将其封装成文件的目的
是为了统一接口,使得读写套接字和读写文件的操作一致
流式套接字(SOCKET_STREAM)
流式套接 字 (SOCKET_STREAM):
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复的发送且按发送顺序接收。内设置流量控制, 避免数据流淹没慢的接收方。数据被看作是字节流,无长度限制。
数据报套接字 (SOCK_DGRAM):
提供无连接服务。数据包以独立数据包的形式被发送,不提供无差错保证,数据可能丢失或重复,顺序发送,可能乱序接收。
原始套接字 (SOCK_RAW):
可以对较低层次协议如 IP 、 ICMP 直接访问。
二、网络通信
本质就是:网络通信的本质是不同主机,不同进程之间的通信。
1. 网络通信的核心要素
要素 | 说明 |
---|---|
IP地址 | 设备的唯一标识(如IPv4的192.168.1.1 ,IPv6的2001:db8::1 )。 |
端口号 | 区分同一设备上的不同服务(范围0~65535,如HTTP=80,SSH=22)。 |
协议 | 规定数据格式和传输规则(如TCP、UDP、HTTP)。 |
套接字(Socket) | 编程接口,用于实现网络通信(如Python的socket 模块)。 |
进程A 需要给进程 B 发送信息,就必须知道进程 B 的IP+端口号 ,因此每个 Socket 都与端口号和协议有关。
三, UDP通信创建流程
UDP是一个传输层的无连接的协议,我们编写代码一般是分为两个端,发送端和接收端。正常一般是接收端先运行,然后等待发送端发送数据
1. 流程图
(1) 服务端(接收端)流程
-
创建UDP Socket
-
绑定IP和端口(
bind()
) -
接收数据(
recvfrom()
) -
处理数据并响应(可选)
-
关闭Socket
(2) 客户端(发送端)流程
-
创建UDP Socket
-
发送数据(
sendto()
) -
接收响应(可选,
recvfrom()
) -
关闭Socket
2、UDP通信相关API函数
1. 创建Socket套接字,实质类似于对文件的操作
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int socket(int domain, int type, int protocol);
描述
创建一个通信的终端节点,并返回一个指向该端点的文件描述符。
参数
domain:选择用于通信的协议族,目前Linux内核可以理解的格式包括AF_UNIX Local communication(本地通信) AF_LOCAL Synonym for AF_UNIX(AF_UNIX的同义词)AF_INET IPv4 Internet protocols(IPv4互联网协议)......
type:套接字类型,指定通信语义,目前定义的类型有SOCK_STREAM Provides sequenced, reliable, two-way, connection-based byte
streams.SOCK_DGRAM Supports datagrams (connectionless, unreliable messages of a
fixed maximum length).......
protocol(/ˈprəʊtəkɒl/协议):协议编号,0表示让系统自动识别
2. 发送数据
头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
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:发送的目标地址的大小
返回值
成功:返回发送的字节数
失败:-1并设置errno
【man 7 ip】
struct sockaddr{__SOCKADDR_COMMON (sa_); /* unsigned short int sa_family (协议族) */char sa_data[14]; /* Address data. */};/* Structure describing an Internet socket address. */
struct sockaddr_in{__SOCKADDR_COMMON (sin_);in_port_t sin_port; /* Port number. (端口号字节序) */struct in_addr sin_addr; /* Internet address.(IP网络字节序) *//* 填充到 `struct sockaddr'的大小 */unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZEsizeof (in_port_t)- sizeof (struct in_addr)];};
struct in_addr {in_addr_t s_addr;
};
typedef unsigned short int sa_family_t;
#define __SOCKADDR_COMMON(sa_prefix) \sa_family_t sa_prefix##family
#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
四, UDP客户端代码实现
#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>
void send_data(int,struct sockaddr_in*,int);
int main(int argc,char* argv[])
{
if(argc !=3){
fprintf(stderr,"command: %s, ip port\n",argv[0]);
exit(EXIT_FAILURE);}
// 1、创建Socket套接字
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socket");
exit(EXIT_FAILURE);}
// 2、将接收地址的 IP+端口 封装成struct sockaddr_in类型
struct sockaddr_in dest_addr;
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&(dest_addr.sin_addr));
// 3、发送数据
send_data(sockfd,&dest_addr,sizeof(dest_addr));
// 4、关闭文件描述符
close(sockfd);
}
void send_data(int sockfd,struct sockaddr_in* pdest_addr,int length)
{
// 循环发送
while(1){
//用户输入发送内容
putchar('>');
char buf[256]={0};
// 清空buf
memset(buf,0,sizeof(buf));
fgets(buf,sizeof(buf),stdin);
// 将回车符转为'\0'
buf[strlen(buf)-1]='\0';
// 发送
int ret = sendto(sockfd,buf,strlen(buf),0,(struct
sockaddr*)pdest_addr,length);
if(ret == -1){
perror("sendto");
exit(EXIT_FAILURE);}
if(strncmp(buf,"quit",4)==0){
break;}}
}
可以使用软件模拟服务器,来判断UDP客户端的代码是否正确
1. 下载
链接: https://pan.baidu.com/s/1-FD4nYgZJlw9nOBrqFm_0A
提取码: 1234


大家可以实现一个小功能;
编写一个UDP 发送方的代码,新建一个 log.txt 的文件,在文件中写入数据,用户读取 log.txt 文件的内容,通过sendto()发送给网络调试助手,并通过网络测试助手接收数据,判断读取的内容是否正确。
五, UDP服务端代码实现
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
void receive_message(int sockfd);
int main(int argc,char* argv[])
{
if(argc !=3){
fprintf(stderr,"command:%s ip port\n",argv[0]);
exit(EXIT_FAILURE);}
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socket");
exit(EXIT_FAILURE);}
// 为socket绑定IP+端口号
struct sockaddr_in addr;
addr.sin_family=AF_INET;
addr.sin_port = htons(atoi(argv[2]));
inet_aton(argv[1],&(addr.sin_addr));
int ret = bind(sockfd,(struct sockaddr*)&addr,(int)sizeof(addr));
if(ret == -1){
perror("bind");
goto release_resources;
exit(EXIT_FAILURE);}
// 接收消息
receive_message(sockfd);
goto release_resources;
return 1;
release_resources:
close(sockfd);
}
void receive_message(int sockfd)
{
char buf[512]={0};
struct sockaddr_in addr;
printf("wait......\n");
while(1){
// 清空
memset(buf,0,sizeof(buf));
socklen_t addrlen = sizeof(addr);
ssize_t bytes = recvfrom(sockfd,buf,sizeof(buf),0,(struct
sockaddr*)&addr,&addrlen);
if(bytes == -1){
perror("recvfrom");}
//打印接收到的消息
printf("src_ip=%s\n",inet_ntoa(addr.sin_addr));
printf("src_port=%d\n",ntohs(addr.sin_port));
printf("content=%s\n",buf);
if(strncmp(buf,"quit",4)==0){
break;}}
}
最终结果;
六, UDP并发服务器之多进程并发
1、常见的服务器类型
1.1. 迭代服务器
大多数 UDP 都是迭代运行,服务器等待客户端的数据,收到数据后处理该数据,送回其应答,在等待下一个客户端请求。

1.2. 并发服务器
并发服务器是指在同一个时刻可以响应多个客户端的请求
本质是创建多线程/多进程 ,对多数用户的信息进行处理
1.3. UDP并发服务器使用的场景
当UDP协议针对客户请求的处理需要消耗过长的时间时,我们期望 UDP 服务器具有某种形式的并发性。
2、UDP多进程并发服务器
1. 场景设计
服务器接收到客户端信息,需要考虑两种情况
<1>A用户的密钥验证请求消息
<2>B用户的数据交互接收消息
2. 框架图
3. 使用场景
当UDP 服务器与客户端交互多个数据报。问题在于每个客户都是往服务器端的同一个的端口发送数据,并用的同一个sockfd 。并发服务器的每一个子进程如何正确区分每一个客户的数据报(涉及到进程的调度问题,如何避免一个子进程读取到不该它服务的客户发送来的数据报)。
解决的方法是服务器(知名端口)等待客户的到来,当一个客户到来后,记下其IP 和 port ,然后服务器 fork 一个子进程,建立一个socket 再 bind 一个随机端口,然后建立与客户端的连接,并处理该客户的请求。父进程继续循环,等待下一个客户的到来。
3、代码实现
在tftpd中就是使用这种技术的 。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#define LOGIN_KEY "root"
#define LOGIN_SUCCESS 1
#define LOGIN_FAILURE 0
int init_socket(const char* ip,const char* port)
{
// 创建socket
int sockfd = socket(AF_INET,SOCK_DGRAM,0);
if(sockfd == -1){
perror("socket");
return -1;}
// 绑定IP + 端口
struct sockaddr_in addr;
memset(&addr,0,sizeof(struct sockaddr_in));
addr.sin_family =AF_INET;
addr.sin_port = htons(atoi(port));
inet_aton(ip,&(addr.sin_addr));
int addrlen = sizeof(struct sockaddr_in);
if(bind(sockfd,(struct sockaddr*)&addr,addrlen)==-1){
fprintf(stderr,"bind failed\n");
return -1;}
return sockfd;
}
int authentication_key(const char* ip,const char* port)
{
int sockfd = init_socket(ip,port);
if(sockfd == -1){
return -1;}
char buf[512]={0};
int len = sizeof(buf);
struct sockaddr_in addr;
int addrlen = sizeof(addr);
int new_sockfd;
// 循环验证用户密钥
while(1){
memset(buf,0,len);
ssize_t recvbytes = recvfrom(sockfd,buf,len,0,(struct
sockaddr*)&addr,&addrlen);
if(recvbytes == -1){
perror("redvfrom");
return -1;}
unsigned char loginstatus = (strncmp(buf,LOGIN_KEY,4)==0)?
LOGIN_SUCCESS:LOGIN_FAILURE;
if(loginstatus == LOGIN_SUCCESS){
pid_t pid = fork();
if(pid == -1){
perror("fork");
return -1;}
else if(pid == 0){
// 执行的是子进程
close(sockfd);//密钥验证成功,不需要sockfd文件描述符
new_sockfd = init_socket(ip,"0");// 绑定0端口,系统
会随机分配一个可用的端口号
sendto(new_sockfd,&loginstatus,sizeof(loginstatus),0,(struct
sockaddr*)&addr,addrlen);
break;}}
else{
// 登录失败,使用原端口回复信息
ssize_t ret =
sendto(sockfd,&loginstatus,sizeof(loginstatus),0,(struct sockaddr*)&addr,addrlen);}}
return new_sockfd;
}
// 接收数据
void recv_data(int sockfd)
{
char buf[512]={0};
struct sockaddr_in client_addr;
socklen_t addrlen = sizeof(client_addr);
while(1){
memset(buf,0,sizeof(buf));
ssize_t recvbytes = recvfrom(sockfd,buf,sizeof(buf),0,(struct
sockaddr*)&client_addr,&addrlen);
if(recvbytes== -1){
perror("recvfrom");
exit(EXIT_FAILURE);}
printf("client ip:%s\n",inet_ntoa(client_addr.sin_addr));
printf("client port:%d\n",ntohs(client_addr.sin_port));
printf("client content:%s\n",buf);
if(strncmp(buf,"quit",4)==0){
break;}}
close(sockfd);
exit(EXIT_SUCCESS);
}
void signal_handler(int signum)
{
// 回收子进程的资源
waitpid(-1,NULL,WNOHANG);
printf("%s\n",strsignal(signum));
}
int main(int argc,char* argv[])
{
if(argc !=3){
fprintf(stderr,"%s ip port.\n",argv[0]);
exit(EXIT_FAILURE);}
// 回收僵尸态的子进程[子进程结束后,会发SIGCHLD信号]
if(signal(SIGCHLD,signal_handler)==SIG_ERR){
perror("signal error.");
exit(EXIT_FAILURE);}
// 验证秘钥
int sockfd = authentication_key(argv[1],argv[2]);
// recv data
recv_data(sockfd);
return 0;
}
客户端不需要进行修改,如此就实现了客户端和服务处理高并发的基础功能
结果: