APUE-网络socket通信
网络通信
大部分网络应用系统可分为两部分:客户(Client)和服务器(Server),网路服务程序架构又两种:CS模式和BS模式。
CS:Client/Server(客户机/服务器)结构,特点:交互性强,具有安全的存取模式,网络通信量低,响应速度快,利于处理大量数据。
BS:Browser/Server(浏览器/服务器)结构,特点:分布性强,维护方便,开发简单且共享性强,总体拥有成本低。
OSI七层模型
OSI模型,即开放式通信系统互联参考模型(Open System Interconnection Reference Model),是国际标准化组织(ISO)提出的一个试图使各种计算机在世界范围内互连为网络的标准框架,简称OSI。
数据在网络中传输的过程实际是封装和解封装的过程,发送方通过各种封装处理,把数据转换成比特流的形式,比特流在信号传输的硬件媒介中传输,接收方再把比特流进行解封装处理。
TCP/IP协议
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/网际协议)是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议,而是指一个由FTP、SMTP、TCP、UDP、IP等协议构成的协议簇,简称TCP/IP协议。
ping命令使用ICMP协议
ip地址在网络层,mac地址在物理层
地址解析协议:
ARP协议:将ip地址翻译为mac地址
RARP协议:将mac地址翻译为ip地址
网络接口层
- 数据封装/解封装成帧,帧中包括了需要传输的数据,发送方和接收方的物理地址(mac地址)以及检错和控制信息。
- 控制帧传输
- 流量控制,控制发送的速度
网络层
IP协议是TCP/IP协议簇中最核心的协议,所有TCP、UDP、ICMP、IGMP协议数据都以IP数据报格式传输。IP协议提供的是不可靠的、无连接的数据传输服务。
IP头默认20个字节
传输层
从传输层向更底层看,各层的协议都是直接或间接的服务于主机于主机之间的通信,而传输层是在进程与进程通信层面上的,传输层有两个重要的协议:TCP(传输控制协议)和UDP(用户数据报协议)。
UDP协议
用户数据报协议,优点:快,缺点:不可靠、不稳定
- 无连接,发送之前不需要建立连接(TCP需要),减少延时和开销
- 面向报文,对IP数据只做简单的封装(8字节报头),减少开销
- 没有阻塞机制,宁愿阻塞时丢弃数据不传,也不阻塞造成延时
- 支持一对一、一对多、多对一、多对多通信
TCP协议
传输控制协议,面向连接、提供可靠的数据传输服务(20字节报文头)。
- 使用前需要进行”三次握手“建立连接,通信结束后还要使用”四次挥手“断开连接。
- 点对点的连接,一条TCP连接只能连接两个端点
- 提供可靠传输,无差错、不丢失、不重复、按顺序
- 提供全双工通信,允许通信双方任何时候都能发送数据,发送方设有发送缓存,接收方有接收缓存
- 面向字节流
应用层
常见端口:
端口号 | 名称 | 说明 |
---|---|---|
20 | ftp-data | FTP数据端口 |
21 | ftp | 文件传输协议(FTP)控制端口 |
22 | ssh | 安全Shell远程登录服务 |
23 | telnet | Telnet远程登录服务 |
25 | smtp | 简单邮件传输协议(STMP) |
53 | dns | 域名服务 |
69 | tftp | 简单文件传输协议 |
80 | http | 用于万维网(www)服务的超文本传输协议 |
123 | ntp | 网络时间协议 |
161/162 | snmp | 简单网络管理协议 |
443 | https | 安全超文本传输协议 |
1433 | mysql | 数据库服务程序默认端口 |
8080 | tomcat | java服务程序默认端口 |
端口号是16位,因此端口号的范围是065535,其中11024是被RFC3232规定好的,监听该范围端口程序必须以root权限运行;1025~65535的端口被称为动态端口,写服务器程序时,一般使用该范围内的端口。可以使用netstat命令查看当前主机上运行了哪些服务程序并监听了哪些端口。
一个TCP的网络连接中包含一个四元组:源IP、目的IP、源端口和目的端口。
socket编程
socket通信简介
网络层的”IP地址“可以唯一标识网络中的主机,而传输层的”端口“可以唯一标识主机中的应用程序。这样通过IP地址和端口就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其他进程进行交互。使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)来实现网络进程之间的通信。
socket是应用层与TCP/IP协议簇通信的中间软件抽象层。
应用程序要为因特网通信建立一个socket时,操作系统就返回一个小整数作为描述符来标识这个套接字。应用程序以改描述符作为传递参数,通过调用相应函数来完成操作。
socket通信基本流程:
socket服务器和客户端代码
服务器代码
#include <stdio.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #define LISTEN_PORT #define BACKLOG int main(int argc, char *argv[]) { int rv = -1; int listen_fd, client_fd = -1; struct sockaddr_in serv_addr,cli_addr; socklen_t cliaddr_len = 1; char buf[1024]; listen_fd = socket(AF_INET,SOCK_STREAM,0); if( listen_fd < 0 ) { printf("creat socket failure: %s\n",strerror(errno)); return -1; } printf("socket creat fd[%d]\n",listen_fd); memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; //sin_family指定ipv4还是ipv6 serv_addr.sin_port = htons(LISTEN_PORT); //sin_port指定端口,htons将主机字节序转为网络字节序 /*要监听的IP,INADDR_ANY即监听所有IP,如果要监听某个IP,用inet_aton()函数转为四字节整数形式,一般用宏*/ serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); /*(struct sockaddr *)&serv_addr结构体类型强制转换,将sockaddr_in转为sockaddr ,sin_port和sin_addr都放在sa_data[14]中 */ if( bind(listen_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0 ) { printf("creat socket failure: %s\n",strerror(errno)); return -2; } printf("socket [%d] bind on port [%d] for all IP address ok\n",listen_fd,LISTEN_PORT); listen(listen_fd,BACKLOG); //把主动的socket变为被动的 while(1) { printf("Start waiting and accept new client connect...\n",listen_fd); client_fd = accept(listen_fd,(struct sockaddr*)&cli_addr,&cliaddr_len); //如果不需要ip和端口则传NULL if( client_fd < 0 ) { printf("accept new socket failure: %s\n",strerror(errno)); return -3; } printf("accept new client[%s:%d] with fd[%d]\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port),client_fd); memset(buf,0,sizeof(buf); if( (rv = read(clien_fd,buf,sizeof(buf))) < 0 ) //< 0出错 { printf("read data from client socket[%d] failure:%s\n",client_fd,strerror(errno)); close(client_fd); continue; } else if( rv == 0) //=0说明断开连接 { printf("client socket[%d] disconnected\n",client_fd); close(client_fd); continue; } printf("read %d bytes data from client[%d] and echo it back:'%s'\n",rv,client_fd,buf); if( write(client_fd,buf,rv) < 0 ) { printf("write %d bytes data back to client[%d] failure: %s\n",rv,client_fd,strerror(errno)); close(client_fd); } sleep(1); close(client_fd); } close(listen_fd); return 0; }
客户端代码
#include <stdio.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERVER_IP "127.0.0.1" #define SERVER_PORT 8889 #define MSG_STR "Hello World!" int main(int argc,char *argv[]) { int conn_fd = -1; int rv = -1; char buf[1024]; struct sockaddr_in serv_addr; conn_fd = socket(AF_INET,SOCK_STREAM,0); if( conn_fd < 0 ) { printf("creat socket failure: %s\n",strerror(errno)); return -1; } printf("soket creat fd[%d]\n",conn_fd); memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERVER_PORT); /*127.0.0.1是点分十进制的字符串,用inet_aton转为四字节整数形式*/ inet_aton(SERVER_IP,&serv_addr.sin_addr); if(connect(conn_fd,(struct sockaddr *)&serv_addr,sizeof(serv_addr)) < 0) { printf("connect to server [%s:%d] failure: %s\n",SERVER_IP,SERVER_PORT,strerror(errno)); return -2; } if( write(conn_fd,MSG_STR,strlen(MSG_STR)) < 0 ) { printf("write data to server [%s:%d] failure: %s\n",SERVER_IP,SERVER_PORT,strerror(errno)); close(conn_fd); } memset(buf,0,sizeof(buf)); rv = read( conn_fd,buf,sizeof(buf)); if(rv < 0) { printf("read data from server faliure: %s\n",strerror(errno)); close(conn_fd); } else if( 0 == rv ) { printf("server disconnected\n"); close(conn_fd); } printf("read %d bytes data from server: '%s'\n",rv,buf); return 0; }
li@Raspberrypi4B:~ $ ./socket_server socket creat fd[3] socket [3] bind on port [8889] for all IP address ok Start waiting and accept new client connect... accept new client[16.4.1.0:15862] with fd [4] read 12 bytes data from client[4] and echo it back:'Hello World!' Start waiting and accept new client connect...
li@Raspberrypi4B:~ $ ./socket_client soket creat fd[3] read 12 bytes data from server: 'Hello World!'
socket()函数
int socket(int doamin,int type,int protocol);
Socket()用于创建一个socket描述符,标识一个socket。
- domain:协议域,决定了socket的地址类型,如 AF_INET要用IPV4地址与端口号
- type:指定socket类型,常见类型:SOCK_STREM(TCP),SOCK_DGRAM(UDP)
- protocol:指定协议,一般默认为0即可,0可以根据第二参数自动匹配相应的协议
bind()函数
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
- sockfd:socket描述符
- addr:sockaddr 类型的指针,储存地址和端口。存储IPv4的结构体类型为:struct sockaddr_in,存储IPv6的结构体类型为:struct sockaddr_in6
- addrlen:地址长度
listen()函数
int listen(int sockfd,int backlog);
- sockfd:要监听的socket描述字
- backlog:相应socket可以排队的最大连接数
accept()函数
int accept(int sockfd,struct sockaddr *addr,socklen_t addrlen)
- sockfd:监听socket描述字
- addr:struct sockaddr*类型的指针
- addrlen:地址长度
- 返回值:一个新的描述符用于和客户端通信
connect()函数
int connect(int sockfd,struct sockaddr *addr,socklen_t addrlen)
- sockfd:客户端的socket描述字
- addr:服务器的socket地址
- addrlen:socket地址长度
新型网路地址转化函数inet_pton和inet_ntop
在此前的代码中我们使用的inet_aton()或inet_ntoa()函数完成IPv4点分十进制字符串和32位整形数据之间的互相转换,但这两个函数只适合于IPv4的地址。下面这两个函数可以同时兼容IPv4和IPv6的地址:
//将点分十进制的ip地址转化为用于网络传输的数值格式,返回值:若成功则为1,若输入不是有效的表达式则为0,若出错则为-1 int inet_pton(int family, const char *strptr, void *addrptr); //将数值格式转化为点分十进制的ip地址格式,返回值:若成功则为指向结构的指针,若出错则为NULL const char * inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
close()和shutdown()函数
int close(int fd);
对socket fd调用close()会触发TCP连接断开的四路挥手。
int shutdown(int sockfd,int how);
shutdown()函数可以半关闭套接字。
how值:
- SHUT_RD:不可再读入数据
- SHUT_WR:不可再写入数据
- SHUT_RDWR:既不可读,也不可写
getopt()和getopt_long()函数
命令行参数
命令行参数可以分为两类,一类是短选项,一类是长选项,短选项在参数前加一杠"-",长选项在参数前连续加两杠"–",如下表(ls 命令参数)所示,其中-a,-A,-b都表示短选项,–all,–almost-all, --author都表示长选项。他们两者后面都可选择性添加额外参数。比如–block-size=SIZE,SIZE便是额外的参数。例如:
LS(1) User Commands LS(1) NAME ls - list directory contents SYNOPSIS ls [OPTION]... [FILE]... DESCRIPTION List information about the FILEs (the current directory by default). Sort entries alphabetically if none of -cftuvSUX nor --sort is speci‐ fied. Mandatory arguments to long options are mandatory for short options too. -a, --all do not ignore entries starting with . -A, --almost-all do not list implied . and .. --author with -l, print the author of each file -b, --escape print C-style escapes for nongraphic characters --block-size=SIZE scale sizes by SIZE before printing them; e.g., '--block-size=M' prints sizes in units of 1,048,576 bytes; see SIZE format below -B, --ignore-backups do not list implied entries ending with ~
getopt_long函数
getopt函数只能处理短选项,而getopt_long函数两者都可以
#include <unistd.h> extern char *optarg; extern int optind, opterr, optopt; #include <getopt.h> int getopt(int argc, char * const argv[],const char *optstring); int getopt_long(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex); int getopt_long_only(int argc, char * const argv[], const char *optstring, const struct option *longopts, int *longindex);
1、argc和argv和main函数的两个参数一致。
2、optstring: 表示短选项字符串。
形式如“a : b : : cd“,分别表示程序支持的命令行短选项有-a、-b、-c、-d,冒号含义如下:
只有一个字符,不带冒号——只表示选项, 如-c
一个字符,后接一个冒号——表示选项后面带一个参数,如-a 100
一个字符,后接两个冒号——表示选项后面带一个可选参数,即参数可有可无, 如果带参数,则选项与参数之间不能有空格,形式应该如-b200
3、longopts:表示长选项结构体。结构如下:
struct option opts[] = { {"ipaddr", required_argument, NULL, 'i'}, {"prot", required_argument, NULL, 'p'}, {"help", no_argument, NULL, 'h'}, {0, 0, 0, 0} };
服务器和客户端代码2
客户端
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <getopt.h> #define MSG_STR "Hello World!" void print_usage(char *progname) { printf("%s usage: \n",progname); printf("-i(--ipaddr): sepcify server IP address.\n"); printf("-p(--port): sepcify server port.\n"); printf("-h(--help): print this help information.\n"); return ; } int main(int argc,char *argv[]) { int conn_fd = -1; int rv = -1; char buf[1024]; struct sockaddr_in serv_addr; char *serv_ip = NULL; int port = 0; int opt = -1; const char *optstring = "i:p:h"; struct option opts[] = { {"ipaddr", required_argument, NULL, 'i'}, {"prot", required_argument, NULL, 'p'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} }; while ( (opt = getopt_long(argc, argv, optstring, opts, NULL)) != -1 ) { switch (opt) { case 'i': serv_ip = optarg; break; case 'p': port = atoi(optarg); break; case 'h': print_usage(argv[0]); return 0; } } if( !serv_ip || !port ) { print_usage(argv[0]); return 0; } conn_fd = socket(AF_INET,SOCK_STREAM,0); if( conn_fd < 0 ) { printf("Creat socket failure: %s\n",strerror(errno)); return -1; } printf("Soket creat fd[%d] successfully\n",conn_fd); // 初始化结构体,将空余的8位字节填充为0 // 设置参数,connect连接服务器 memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(port); inet_aton(serv_ip,&serv_addr.sin_addr); rv = connect( conn_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr) ); if(rv < 0) { printf("Connect to server [%s:%d] failure: %s\n",serv_ip,port,strerror(errno)); return -2; } printf("Connect to server [%s:%d] successfully!\n",serv_ip,port); while(1) { // 每次进入循环清空缓冲区 // 持续读取服务器发送的数据存入缓冲区 rv = write(conn_fd,MSG_STR,strlen(MSG_STR)); if( rv < 0 ) { printf("Write data to server [%s:%d] failure: %s\n",serv_ip,port,strerror(errno)); break; } memset(buf,0,sizeof(buf)); rv = read( conn_fd,buf,sizeof(buf) ); if(rv < 0) { printf("Read data from server by sockfd[%d] faliure: %s\n",conn_fd,strerror(errno)); break; } else if( 0 == rv ) { printf("Sockfd[%d] get disconnected\n",conn_fd); break; } else if( rv > 0 ) { printf("Read %d bytes data from server: '%s'\n",rv,buf); } } close(conn_fd); return 0; }
服务器端
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <getopt.h> #define MSG_STR "Hello World!" #define MAX_CLIENT 5 void print_usage(char *progname) { printf("%s usage: \n",progname); printf("-p(--port): sepcify server port.\n"); printf("-h(--help): print this help inforation.\n"); return ; } int main(int argc,char *argv[]) { int sock_fd = -1; int client_fd = -1; int rv = -1; char buf[1024]; struct sockaddr_in serv_addr; struct sockaddr_in cli_addr; socklen_t cliaddr_len; int port = 0; int on = 1; int opt = -1; const char *optstring = "p:h"; struct option opts[] = { {"prot", required_argument, NULL, 'p'}, {"help", no_argument, NULL, 'h'}, {NULL, 0, NULL, 0} }; while ( (opt = getopt_long(argc, argv, optstring, opts, NULL)) != -1 ) { switch (opt) { case 'p': port = atoi(optarg); break; case 'h': print_usage(argv[0]); return 0; } } if( !port ) { print_usage(argv[0]); return 0; } sock_fd = socket(AF_INET,SOCK_STREAM,0); if( sock_fd < 0 ) { printf("Creat socket failure: %s\n",strerror(errno)); return -1; } printf("Soket creat fd[%d] successfully\n",sock_fd); //解决Address already in use 的问题 setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)); memset(&serv_addr,0,sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(port); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); rv = bind( sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr) ); if(rv < 0) { printf("Socket[%d] bind on port [%d] failure: %s\n",sock_fd,port,strerror(errno)); return -2; } printf("Socket[%d] bind on port [%d]successfully!\n",sock_fd,port); listen(sock_fd,MAX_CLIENT); memset(&cli_addr,0,sizeof(cli_addr)); while(1) { printf("Wating for client connect...\n"); client_fd = accept(sock_fd,(struct sockaddr*)&cli_addr,&cliaddr_len); if( client_fd < 0 ) { printf("Accept client connect failure: %s\n",strerror(errno)); break; } printf("Accept client connect from [%s:%d]\n",inet_ntoa(cli_addr.sin_addr),ntohs(cli_addr.sin_port) ); memset(buf,0,sizeof(buf)); rv = read( client_fd,buf,sizeof(buf) ); if(rv < 0) { printf("Read data from server by sockfd[%d] faliure: %s\n",client_fd,strerror(errno)); close(client_fd); continue; } else if( 0 == rv ) { printf("Sockfd[%d] get disconnected\n",client_fd); close(client_fd); continue; } else if( rv > 0 ) { printf("Read %d bytes data from server: '%s'\n",rv,buf); } rv = write(client_fd,MSG_STR,strlen(MSG_STR)); if( rv < 0 ) { printf("write to client by sockfd[%d] failure: %s\n",sock_fd,strerror(errno)); close(client_fd); continue; } printf("Close client socket[%d]\n",client_fd); close(client_fd); } close(sock_fd); return 0; }
socket域名解析
在我们写网络socket的客户端的时候,我们一般直接使用的是服务器端的IP地址,这相对来说具有一定的局限性,我们可以通过socket的域名解析函数来实现域名解析。
getaddrinfo()
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
node: hostname,域名或者IP地址
service:服务名,可以是十进制端口号,也可以是已定义的服务器名称,比如http等
hints:可以是一个空指针,也可以是一个指向某个addrinfo结构体的指针
res:函数通过result指针参数返回一个指向addrinfo结构体链表的指针,结构体内存储着返回的相关信息
函数返回值:返回的是一个addrinfo的结构体指针,但这个结构体内存储着相应的域名的IP,0——成功,非0——失败
struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; struct sockaddr *ai_addr; //信息存在这个结构体中 char *ai_canonname; struct addrinfo *ai_next; };
gethostbyname()
用域名或主机名获取IP地址
struct hostent *gethostbyname(const char *name);
这个函数的传入值是域名或者主机名。返回值是一个hostent的结构体。如果函数调用失败,返回NULL。结构如下:
struct hostent { char *h_name; //主机的规范名 char **h_aliases; //主机的别名 int h_addrtype; //主机ip地址的类型,到底是ipv4(AF_INET),还是pv6(AF_INET6) int h_length; //主机ip地址的长度 char **h_addr_list; /*主机的ip地址,注意,这个是以网络字节序存储的。不能直接用printf带%s参数来打这个东西。所以到真正需要打印出这个IP的话,需要调用inet_ntop()。*/ };
本文作者:梨子Li
本文链接:https://www.cnblogs.com/LiBlog--/p/17964291
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!