网络通信
大部分网络应用系统可分为两部分:客户(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);
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;
};
getnameinfo()
int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *serv, size_t servlen, int flags);
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()。*/
};
标签:serv,addr,int,APUE,通信,fd,printf,socket
From: https://www.cnblogs.com/LiBlog--/p/17964291