本文导航
内容 | |
---|---|
IP,端口号,网络字节序列基本概念 | |
初步认识TCP,UDP协议 | |
socket API的基本用法 | |
实现一个简单的UDP客户端端/服务器 |
IP地址
概念
- IP地址是在IP协议中, 用来来标识网络络中不同主机的地址;
- 对于IPv4来说, IP地址是一个4字节, 32位的整数;
- 我们通常也使用 “点分十进制” 的字符串表示IP地址, 例如 192.168.0.1 ; 用点分割的每一个数字表示一个字节, 范围是 0 - 255;
源IP地址和目标IP地址
我们光有IP地址就可以完成通信了嘛?
想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上, 但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析。这里就需要两个IP地址。才能完成通信
在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.
源IP,就是发送方的IP地址,目的IP就是接收方的IP地址。
认识端口号
- 端口号(port)是传输层协议的内容.
- 端口号是一个2字节16位的整数;
- 端口号用来标识一个进程(比如QQ), 告诉操作系统, 当前的这个数据要交给哪一个进程来处理;
- IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
- 一个端口号只能被一个进程占用,但是一个进程可以占用多个端口号
就好像一个学生在不同的学校有不同的学号,但是,一个学生无论在哪里上学,都只有一个身份证号码,这个身份证号就相当于IP地址号
源端口号和目的端口号
传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 “数据是谁发的, 要发给谁”;
TCP和UDP协议初识
此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一 个直观的认识; 后我们再详细讨论TCP的一些细节问题.
TCP
- 传输层协议
- 有连接
- 可靠传输(数据无差错传输)
- 面向字节流(写多少读多少)
此处我们也是对UDP(User Datagram Protocol 用户数据报协议)有一个直观的认识再详细讨论.
UDP
- 传输层协议
- 无连接
- 不可靠传输(存在丢包情况,但是有点是速度快)
- 面向数据报(写满后再读)
网络字节序列
需求分析
如果网络中网络字节序列没有任何约束条件,发送方看见一堆数据,就一股脑全给接收方发去,而接受方呢,也不假思索的收数据,不进行任何数据整理,那么,一来二去,数据就乱套了,无法正常工作。
为了解决这个问题,有关人员就为数据的接受和发送指定了一套的规则。
规则
我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘中件中的多字节数据相对于文件中的偏移地址也有小端大端之分, 网络数据流同样有大端小端之分.
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;(数据从内存搬移到网卡,然后发送出去)
接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存(网卡从网络中接收数据,存放在内存中);
- 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
TCP/IP协议规定,网络数据流应采大端字节序,即低地址存字节.
规则实施
不管这台主机是大端机还是大端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;
实施规则工具
(使具有代码可移植性,可以调用以下函数用作网络字节序列和主机字节序列转换
#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表示network,l表示32位⻓整数,s表示16位短整数。
例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。
socket 编程接口用法介绍
socket 常用API
- 创建socket文件
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
参数:
domain:域参数指定一个通信域;这选择了协议族。默认AF_INET,表示IPv4 网络协议
type:指定通信语义,默认SOCK_DGRAM,表示UDP通信
proctocol:协议指定与套接字一起使用的特定协议。默认为0,支持给定协议家族中的特定套接字类型
返回值
在成功时,将返回新套接字的文件描述符。在错误中,返回-1,并且。
errno设置适当。
- 绑定端口
当一个套接字使用套接字(2)创建时,它存在于一个名称空间(地址族)中,但没有分配给它的地址。
bind()将addr指定的地址分配给套接字。由文件描述符sockfd引用。addrlen指定了字节的大小。
地址结构由addr指向。传统上,这种操作称为“分配a”。
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
返回值:成功返回0,失败返回-1
下面我们来认识一下socketaddr结构
struct sockaddr {
sa_family_t sa_family;//地址家族
char sa_data[14];//地址数据(包含端口号和ip地址)
}
struct sockaddr_in {
sa_family_t sin_family; //地址家族
in_port_t sin_port; //端口号
struct in_addr sin_addr; //ip地址
};
struct sockaddr_un {
sa_family_t sun_family; //地址家族
char sun_path[UNIX_PATH_MAX]; //路径名
};
下面总结了一张图简介明了。对比这三种结构
在了解了相关接口后,我们就可以来编写UDP套接字客户端/服务器了
客户端/服务器因为涉及到发送请求和处理请求,所以还需要指定的发送接口和接受接口。
接受请求
#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);
返回值:成功调用返回接收到的字节数,如果出现错误,则返回-1。
发送接口
#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);
返回值:在成功时,这些调用返回发送的字符数。错误,返回-1,
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);
UDP服务器/客户端通信
udp_server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
int main(int argc,char ** argv)
{
if(argc!=3)
{
printf("%s[ip][ port]\n",argv[0]);
return 1;
}
//创建socket文件(打开网卡)
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
perror("socket error!");
return 2;
}
struct sockaddr_in local;
//设置地址家族
local.sin_family=AF_INET;
//设置端口号,并转换成大端模式
local.sin_port=htons(atoi(argv[2]));
//设置ip地址,
local.sin_addr.s_addr=inet_addr(argv[1]);
//绑定端口号
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind error!");
return 3;
}
//定义缓冲区
char buf[128];
//确立客户端
struct sockaddr_in client;
//死循环保证服务器一直处于工作状态
while(1)
{
socklen_t len=sizeof(client);
//从客户端接受数据
ssize_t s=recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&client,&len);
if(s>0)
{
//将接收i到的字符串以\0结尾
buf[s]=0;
//打印接收到的内容输出到屏幕
printf("[%S:%d]:%s\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port),buf);
//将接收到的数据再发送给客户端
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client));
}
}
}
将服务端运行
udp_client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
int main(int argc,char ** argv)
{
if(argc!=3)
{
printf("%s[ip][ port]\n",argv[0]);
return 1;
}
//创建socket文件(打开网卡)
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
perror("socket error!");
return 2;
}
struct sockaddr_in server;
//设置地址家族
server.sin_family=AF_INET;
//设置端口号,并转换成大端模式
server.sin_port=htons(atoi(argv[2]));
//设置ip地址,
server.sin_addr.s_addr=inet_addr(argv[1]);
//定义缓冲区
char buf[1024];
//确定服务端
struct sockaddr_in peer;
while(1)
{
socklen_t len=sizeof(peer);
//从键盘读数据,写进缓冲区
printf( "please enter#");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf)-1);
if(s>0)
{
buf[s-1]=0;
//将缓冲区的数据发送给客户端
sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server));
//接收客户端发送的数据,并输出到屏幕上
ssize_t _s=recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&peer,&len);
if(_s>0)
{
buf[_s]=0;
printf("server echo$# %s\n",buf);
}
}
}
}
用本地回环测试没问题