文章目录
网络基础
所有的网络编程都是基于客户端和服务端的架构
网络协议也就是规则。
水晶头大小及网卡接线都是遵守的一种电气协议,而以太网卡、令牌环网卡则都是遵守一种逻辑协议。
网络通讯采用的协议是TCP/IP协议簇。
分别有:
- osi七层模型
- TCP/IP协议分为四层或者五层:
应用层
传输层
网络层
链路层(物理层)
-
物理层
规定了网络中使用到的电气协议。 -
链路层
规定了网络帧的格式。 -
补充:
网络地址分为:
ip地址 32位
网卡的MAC地址 6字节 48位 物理地址
使用ifconfig可以查看一台机器的ip地址和物理地址。
网络中使用到的三种设备
- HUB集线器
只是将电信号放大分流,属于物理层。 - 交换机
交换的是网帧,属于链路层。 - 路由器
交换的是IP包,属于网络层。
IP地址和IP地址的分类
IPV4 0~255.0~255.0~255.0~255 4G
IPV6 128位 4G*4G*4G*4G
IP地址包含两部分内容:
- 主机号 确定了在网络中的编号
- 网络号 确定了ip地址属于的网络
A类 网络号 0~127 主机号
主机号全0 网络段 主机号全1 本网段的广播地址
B类 网络号128~191 主机号 65534台机器
C类 网络号192~223 主机号 254台机器
网络通讯中需要自动找出ip地址的网络号
使用子网掩码找出ip地址的网络号
192.168.1.130/24
192.168.1.130/255.255.255.0
ip地址和子网掩码做与操作,结果就是ip地址的网络号
192.168.1.0
IP地址:192.168.1.130/25
子网掩码:255.255.255.128
192.168.1.128 192.168.1.129~192.168.1.254
IP地址:192.168.1.125/25
子网掩码:255.255.255.128
192.168.1.0 192.168.1.1~192.168.1.126
数据在局域网中如何传输
- 判断目标ip地址和自己的ip地址是否是同一网段
- 在路由表中查找是否要出网
- 在本机的arp表中查找目标ip地址的MAC地址。
- 如果arp表中有目标ip地址的MAC地址。将数据发送到目标
- 如果arp表中没有目标ip地址的MAC地址。本机发起广播,询问谁的ip地址是目标地址。目标机器接收到消息,回应自己的mac地址给广播者。这时候广播者将数据发送过去。
数据跨网段传输
网关
路由表
sudo route
arp表
sudo arp -a
测试通讯路径是否畅通。使用ping命令
ping 目的ip地址
127.0.0.1 测试本机的网络设备是否正常。
环回地址
三次握手
和对方通讯的时候,需要知道对方的ip地址和端口号
基于TCP的网络编程
基于TCP的编程模型
TCP服务器
- 创建通讯端socket,获取通讯描述符。
socket(2)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建通讯端
参数:
domain:
AF_INET:IPV4 协议簇
AF_INET6:IPV6 协议簇
type:
SOCK_STREAM:tcp的格式
SOCK_DGRAM: udp的格式
protocol:
0
返回值:
-1 错误 errno被设置
新的文件描述符
- 将通讯描述符和服务器的IP地址及端口号绑定
bind(2)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:将通讯描述符和服务器的地址空间绑定
参数:
sockfd:通过socket(2)获取到的通讯描述符
addr:指定了服务器的地址空间
addrlen:指定了addr参数的地址空间的长度
返回值:
0 成功
-1 错误 errno被设置
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};
- 监听通讯描述符
listen(2)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:在sockfd上监听即将到来的连接
参数:
sockfd:指定通讯描述符
backlog:最大的未决连接数
返回值:
0 成功
-1 错误 errno被设置
- 阻塞等待客户端请求的到来,获取连接描述符
accept(2)
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr,\
socklen_t *addrlen);
功能:在sockfd上接收连接
参数:
sockfd:通讯描述符 socket(2)的返回值
addr: 客户端的地址空间填充addr指定的空间里。
如果addr指定为NULL,那么addrlen也需要指定为NULL。
addrlen:客户端地址空间的尺寸
返回值:
-1 错误 errno被设置
连接描述符(使用这个连接描述符和客户端进行通讯)。
- 获取客户端的数据
read(connfd,,);
- 数据处理
- 将处理后的数据返回客户端
write(connfd,,);
- 关闭和客户端的连接。
close(connfd);
- 补充:
- 主机字节序、网络字节序(大端)
htonl(3)
htons
ntohl
ntohs
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h host
n net
l long
s short
- 具体的网络地址空间和通用的网络地址空间转换
具体网络协议族有ipv4 ipv6 local unix。
通用的地址空间 struct sockaddr类型的
bind、accept函数中的参数需要的是通用地址空间。
int *p; char *q;
void *.
使用函数inet_pton(3)
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
功能:text to binary
参数:
af:
AF_INET:IPV4
AF_INET6:IPV6
src: 字符串格式的ip地址
dst:strcut in_addr类型的变量
返回值:
1 成功
0 字符串错误
-1 失败 errno被设置
inet_ntop(3)
#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
功能:binary to text
参数:
af:
AF_INET:IPV4
AF_INET6:IPV6
src: struct in_addr
dst:字符串地址空间
size:指定了buf的有效字节数
返回值:
NULL 错误 errno被设置
指向dst空间的地址
- sockaddr_in
struct sockaddr_in{
sa_family_t sin_family AF_INET.
in_port_t sin_port Port number.
struct in_addr sin_addr IP address.
};
struct in_addr{
in_addr_t s_addr;
};
代码示例
- server.c
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <ctype.h>
#include <arpa/inet.h>
int main(void){
int s_fd,conn_fd;
char buf[128];
struct sockaddr_in server;
//创建socket
s_fd=socket(AF_INET,SOCK_STREAM,0);
if(s_fd==-1){
perror("socket");
return 1;
}
//对服务器的地址和端口号初始化
server.sin_family=AF_INET;
server.sin_port=htons(7777);
//INADDR_ANY代表本机上的所有ip地址
server.sin_addr.s_addr=htonl(INADDR_ANY);
//绑定服务器的地址和端口号到通讯描述符
int b=bind(s_fd,(struct sockaddr *)&server,\
sizeof(server));
if(b==-1){
perror("bind");
return 2;
}
//在s_fd描述符上监听
listen(s_fd,5);
while(1){
//阻塞等待客户端连接的到来
conn_fd=accept(s_fd,NULL,NULL);
if(conn_fd==-1){
perror("accept");
return 3;
}
//获取客户端的数据
int r=read(conn_fd,buf,128);
//处理数据
for(int i=0;i<r;i++)
buf[i]=toupper(buf[i]);
//发送给客户端
write(conn_fd,buf,r);
//关闭连接
close(conn_fd);
}
close(s_fd);
return 0;
}
TCP客户端
- 使用socket创建通讯描述符
- 使用connect链接服务器
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:连接sockfd到服务器的地址空间
参数:
sockfd:socket(2)的返回值
addr: 服务器的地址空间
addrlen:addr的空间长度
返回值:
0 成功
-1 失败 errno被设置
- 使用write向服务器发送数据。
- 等待服务器的响应。
- 关闭和服务器的链接。
代码示例
- client.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
int main(void){
struct sockaddr_in server;
char msg[]="this is first\n";
char buf[128];
//创建socket通讯描述符
int s_fd=socket(AF_INET,SOCK_STREAM,0);
if(s_fd==-1){
perror("socket");
return 1;
}
//初始化服务器的地址和端口号
server.sin_family=AF_INET;
server.sin_port=htons(7777);
//127.0.0.1
inet_pton(AF_INET,"127.0.0.1",\
&server.sin_addr);
//连接s_fd到服务器的地址
int c=connect(s_fd,\
(struct sockaddr *)&server,\
sizeof(server));
if(c==-1){
perror("connect");
return 2;
}
//向服务器发送数据
write(s_fd,msg,strlen(msg));
int r=read(s_fd,buf,128);
write(1,buf,r);
close(s_fd);
return 0;
}
- 执行结果
修改TCP客户端代码
修改为可以使用参数连接服务器地址及通过标准输入获取发送字符串。具体如下:
- client_pro.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc,char *argv[]){
struct sockaddr_in server;
char msg[128];
char buf[128];
//创建socket通讯描述符
int s_fd=socket(AF_INET,SOCK_STREAM,0);
if(s_fd==-1){
perror("socket");
return 1;
}
//初始化服务器的地址和端口号
server.sin_family=AF_INET;
server.sin_port=htons(7777);
//127.0.0.1
inet_pton(AF_INET,argv[1],\
&server.sin_addr);
//连接s_fd到服务器的地址
int c=connect(s_fd,\
(struct sockaddr *)&server,\
sizeof(server));
if(c==-1){
perror("connect");
return 2;
}
#if 0
//向服务器发送数据
write(s_fd,msg,strlen(msg));
//阻塞等待服务器响应信息的到来
int r=read(s_fd,buf,128);
write(1,buf,r);
#endif
/*一次连接可以交互多次*/
while(fgets(msg,128,stdin)!=NULL){
write(s_fd,msg,\
strlen(msg)+1);
if (strcmp(msg,"quit\n")==0)
break;
int r=read(s_fd,buf,128);
write(1,buf,r);
printf("\n");
}
close(s_fd);
return 0;
}
- 执行结果
并发服务器的实现
- 使用线程实现并发服务器
- 多路复用技术实现服务器的并发(select 、poll)
- 使用多进程实现服务器的并发(父进程负责监听客户端连接的到来,子进程负责处理客户端请求,fork在accept后添加合适)
修改基本服务器代码
- server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <string.h>
int t_listen(short port,int backlog){
int s_fd;
struct sockaddr_in server;
s_fd=socket(AF_INET,SOCK_STREAM,0);
if(s_fd==-1){
perror("socket");
return -1;
}
//对服务器的地址和端口号初始化
server.sin_family=AF_INET;
server.sin_port=htons(port);
//INADDR_ANY代表本机上的所有ip地址
server.sin_addr.s_addr=htonl(INADDR_ANY);
//绑定服务器的地址和端口号到通讯描述符
int b=bind(s_fd,(struct sockaddr *)&server,\
sizeof(server));
if(b==-1){
perror("bind");
return -1;
}
//在s_fd描述符上监听
listen(s_fd,backlog);
return s_fd;
}
int main(void){
int s_fd,conn_fd;
char buf[128];
char IP[128];
int len;
struct sockaddr_in client;
s_fd=t_listen(7777,5);
while(1){
len=sizeof(client);
//阻塞等待客户端连接的到来
conn_fd=accept(s_fd,\
(struct sockaddr *)&client,&len);
if(conn_fd==-1){
perror("accept");
return 3;
}
printf("%s\n",\
inet_ntop(AF_INET,&client.sin_addr,IP,128));
while(1){
//获取客户端的数据
int r=read(conn_fd,buf,128);
//strcmp(buf,"q");
//处理数据
for(int i=0;i<r;i++)
buf[i]=toupper(buf[i]);
if(strcmp(buf,"QUIT\n")==0)
break;
//发送给客户端
write(conn_fd,buf,r);
}
//关闭连接
close(conn_fd);
}
close(s_fd);
return 0;
}
- 执行结果
多进程实现服务器并发
- bserver.c
/*************************************************************************
> File Name: server.c
> Author:
> Mail:
> Created Time: 2020年02月07日 星期五 08时42分20秒
************************************************************************/
#include<stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int t_listen(short port, int backlog)
{
int s_fd;
struct sockaddr_in server;
s_fd = socket(AF_INET, SOCK_STREAM, 0);
if(s_fd < 0)
{
perror("socket");
return -1;
}
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = htonl(INADDR_ANY);
int b = bind(s_fd, (struct sockaddr *)&server, sizeof(server));
if(b < 0)
{
perror("bind");
return -2;
}
listen(s_fd, backlog);
return s_fd;
}
int main()
{
int s_fd, conn_fd;
char buf[128];
int len;
char IP[128];
struct sockaddr_in client;
s_fd = t_listen(7777, 5);
while(1)
{
len = sizeof(client);
conn_fd = accept(s_fd, (struct sockaddr *)&client, &len);
if(conn_fd < 0)
{
perror("accept");
return -3;
}
printf("client:%s\n", inet_ntop(AF_INET, &client.sin_addr, IP, 128));
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
return -4;
}
else if(pid == 0)
{
// child pid
close(s_fd);
while(1)
{
int r = read(conn_fd, buf, 128);
for(int i = 0; i < r; i++)
{
buf[i] = toupper(buf[i]);
}
if(strcmp(buf, "QUIT\n") == 0)
{
break;
}
write(conn_fd, buf, r);
}
close(conn_fd);
exit(0);
}
else{
//parent pid
close(conn_fd);
waitpid(-1, NULL, WNOHANG);
}
}
close(s_fd);
return 0;
}
- 执行结果
基于UDP的编程
UDP客户端流程
- 创建通讯描述符
s_fd=socket(AF_INET,SOCK_DGRAM,0);
- 使用通讯描述符向服务器端发送数据
sendto(2)
- 等待服务器端的响应信息
recvfrom(2)
- 关闭通讯描述符
close(s_fd);
UDP的服务器流程
- 创建通讯描述符
s_fd=socket(AF_INET,SOCK_DGRAM,0);
- 将通讯描述符和服务器地址空间绑定
bind(s_fd,(struct sockaddr *)&server,sizeof(server));
- recvfrom等待客户端数据的到来
recvfrom(2)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, \
int flags,struct sockaddr *src_addr,\
socklen_t *addrlen);
功能:从socket接受消息
参数:
sockfd:socket(2)的返回值
buf:指定了接收消息的空间地址
len:接收消息的长度
flags:0
src_addr:保存对面的地址空间,如果指定为NULL。那么addrlen也要之定位空。
addrlen:指定src_addr变量空间的长度。
返回值:
接收到的字节数
-1 错误产生
对面正常关闭返回0
- 处理客户端的数据
- 回应客户端消息
sendto(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);
功能:在socket上发送一个消息
参数:
sockfd:socket(2)的返回值
buf:buf指定了消息存放的地址
len:指定要发送的消息的长度
flags:0
dest_addr:目标地址
addrlen:目标地址的长度
返回值:
-1 错误 errno被设置
发送出去的字节数
代码示例
- userver.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <ctype.h>
typedef struct sockaddr SA;
typedef struct sockaddr_in SA4;
int main(void){
int s_fd;
SA4 server,client;
char buf[128];
int r;
//创建通讯描述符
s_fd=socket(AF_INET,SOCK_DGRAM,0);
if(s_fd==-1){
perror("socket");
return 1;
}
//初始化server的地址空间
server.sin_family=AF_INET;
server.sin_port=htons(7776);
server.sin_addr.s_addr=htonl(INADDR_ANY);
//将通讯描述符和服务器的地址空间绑定
int b=bind(s_fd,(SA *)&server,\
sizeof(server));
if(b==-1){
perror("bind");
return 2;
}
int clilen=sizeof(client);
//等待客户端请求信息的到来
while((r=recvfrom(s_fd,buf,128,0,\
(SA *)&client,&clilen))>0){
//数据处理
for(int i=0;i<r;i++)
buf[i]=\
toupper(buf[i]);
//为客户端回送处理后的消息
sendto(s_fd,buf,r,0,\
(SA *)&client,\
sizeof(client));
}
//永远执行不到
close(s_fd);
return 0;
}
- uclient.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
typedef struct sockaddr SA;
typedef struct sockaddr_in SA4;
int main(int argc,char *argv[]){
int s_fd;
SA4 server;
char buf[]="this is a test\n";
char rbuf[128];
//创建通讯描述符
s_fd=socket(AF_INET,SOCK_DGRAM,0);
if(s_fd==-1){
perror("socket");
return 1;
}
//初始化服务器的地址空间
server.sin_family=AF_INET;
server.sin_port=htons(7776);
inet_pton(AF_INET,argv[1],&server.sin_addr);
//向服务器发送消息
sendto(s_fd,buf,strlen(buf)+1,0,(SA *)&server,\
sizeof(server));
//从服务器端获取响应消息
int r=recvfrom(s_fd,rbuf,128,0,NULL,NULL);
write(1,rbuf,r);
printf("\n");
close(s_fd);
return 0;
}
- 执行结果