认识一下IP地址和端口号
1、IP地址
IP地址有两个版本,IPv4和IPv6,IPv4版本占四个字节,32个比特位;IPv6版本占64个字节,128个比特位,大部分情况下,默认使用IPv4版本。在IP的数据报头中包含了两个IP地址,源IP地址和目的IP地址,其中源IP地址表示从哪台主机上面发送数据,目的IP表示发送到那台主机上面。但是仅仅只有IP地址是行不通的,我们只是把数据转发到了另一台主机上,并不知到要把数据发送给哪一个进程进行解析,这时,我们就引入了端口号。
2、端口号
·端口号时传输层协议,端口号占两个字节,16个比特位的整数
·端口号用来标识一个唯一的进程,告诉操作系统当前接收到的数据需要交给哪个进程来处理
一个端口号只能被一个进程占用
3、IP地址与端口号之间的关系
IP地址表示互联网主机众多主机中的唯一一台主机
端口号表示主机中众多进程中的唯一进程
IP地址+端口号可以标识某一台主机上的某一个进程
4、端口号与进程ID之间的关系
进程ID用来唯一标识一个进程,而端口号只是用来绑定一个进程。
一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定
源端口号与目的端口号的理解
表示从某一台主机中的某一个进程与另一台主机的一个进程之间的通信
她们分别表示数据是谁发的,具体要发给谁。
套接字用来表示两个进程之间的通信,用IP地址+端口号来表示
认识TCP和UDP
TCP的特点:传输层协议,有连接,可靠传输,面向字节流(即可以一个字节一个字节的发送)
UDP 的特点:传输层协议,无连接,不可靠传输,面向数据报(即不能以字节的方式取走收据,必须一次性拿走所有的数据报文)
网络字节序列
内存中的多字节数据相对于内存地址有大小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大小端之之分,网络数据流同样也有大小端之分,下面我们来了解网络数据流的地址
1、发送主机通常将缓冲区的数据按内存地址由低到高的顺序发出
2、接收主机把网络上接受到的字节依次保存在缓冲区中,也按内存地址由低到高的顺序保存
3、因此网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高地址。
4、TCP/IP协议规定,网络数据流应采用大端字节,即低地址高字节
5、小端主机要转成大端才可以向另一台主机发送数据
对大小端存储的理解
大端存储:数据的高字节保存在内存的低地址中
小段存储:数据的高字节保存在内存的高地址中
为了使网络具有可移植性,可以调用以下函数做网络字节序和主机字节序之间的转换
#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);
//后两组表示网络接受主机发送的数据,将网络序转换成主机序
在不需要进行转换的情况下,这些函数不做任何处理直接返回
socket编程
1、什么是API?
API(Application Programming Interface,应用程序编程接口)是一些预先定义的函数,目的是提供应用程序与开发人员基于某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。
2、了解sockaddr结构体
用于存储参与(IP)Windows套接字通信的计算机上的一个internet协议(IP)地址。为了统一地址结构的表示方法 ,统一接口函数,使得不同的地址结构可以被bind()、connect()、recvfrom()、sendto()等函数调用。但一般的编程中并不直接对此数据结构进行操作,而使用另一个与之等价的数据结构sockaddr_in。这是由于Microsoft TCP/IP套接字开发人员的工具箱仅支持internet地址字段,而实际填充字段的每一部分则遵循sockaddr_in数据结构,两者大小都是16字节,所以二者之间可以进行切换。
sockaddr结构体
struct sockaddr {
unsigned short sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};
说明:
sa_family :是2字节的地址家族,一般都是“AF_xxx”的形式,它的值包括三种:AF_INET,AF_INET6和AF_UNSPEC。
如果指定AF_INET,那么函数就不能返回任何IPV6相关的地址信息;如果仅指定了AF_INET6,则就不能返回任何IPV4地址信息。
AF_UNSPEC则意味着函数返回的是适用于指定主机名和服务名且适合任何协议族的地址。如果某个主机既有AAAA记录(IPV6)地址,同时又有A记录(IPV4)地址,那么AAAA记录将作为sockaddr_in6结构返回,而A记录则作为sockaddr_in结构返回
通常用的都是AF_INET
sockaddr_in结构体
struct sockaddr_in {
short int sin_family; /* Address family */
unsigned short int sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
unsigned char sin_zero[8]; /* Same size as struct sockaddr */
};
sin_family:指代协议族,在socket编程中只能是AF_INET
sin_port:存储端口号(使用网络字节顺序)
sin_addr:存储IP地址,使用in_addr这个数据结构
sin_zero:是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。
虽然socket API的接口是sockaddr,但我们真正基于IPV4编程时,使用的数据结构是sockaddr_in;这个结构体主要包括包括十六位地址类型,16位端口号,32位IP地址
in_addr结构体
typedef struct in_addr {
union {
struct{ unsigned char s_b1,s_b2, s_b3,s_b4;} S_un_b;
struct{ unsigned short s_w1, s_w2;} S_un_w;
unsigned long S_addr;
} S_un;
} IN_ADDR;
阐述下in_addr的含义,很显然它是一个存储ip地址的共用体有三种表达方式:
第一种用四个字节来表示IP地址的四个数字;
第二种用两个双字节来表示IP地址;
第三种用一个长整型来表示IP地址。
给in_addr赋值的一种最简单方法是使用inet_addr函数,它可以把一个代表IP地址的字符串赋值转换为in_addr类型,如addrto.sin_addr.s_addr=inet_addr(“192.168.0.2”);
其反函数是inet_ntoa,可以把一个in_addr类型转换为一个字符串。
**·**IPV4和IPV6的地址格式定义在netinet.h中,IPV4地址用sockaddr_in结构体表示,包括十六位地址类型,16位端口号,32位IP地址
**·**IPV4、IPV6地址类型分别定义为常数AF_INET,AF_INET6,这样只要取得某种sockaddr的结构体的首地址,不需要具体知道那种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
· socket API 可以都用struct sockaddr*类型表示,在使用的时候需要强制转化成sockaddr_in,这样的好处是程序的通用性,可以接受IPV4,IPV6,以及UNIX Domain Socket各种类型的sockaddr结构体指针作为参数
3、socket常见API
//创建socket文件描述符,该函数返回一个文件描述符,(TCP/UDP客户端+服务器)
int socket(int domain, int type, int protocol);
//绑定端口号(TCP/UDP服务器)
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
//接受请求(TCP服务器)
int accept(int sockfd, struct sockaddr *addr, socklen* addrlen);
//建立连接(TCP客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
实现简答版本的UDP
server.c
#include<stdio.h>
#include<arpa/inet.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include<netinet/in.h>
#include<error.h>
#include<unistd.h>
#include<string.h>
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(atoi(argv[2]));
local.sin_addr.s_addr=inet_addr(argv[1]);//sin_addr是一个结构体
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage: %s [ip] [port]",argv[1]);
return 3;
}
int sock=socket(AF_INET,SOCK_DGRAM,0);//创建一个端口号
if(sock<0)
{
perror("socket");
return 2;
}
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)//绑定端口号
{
perror("bind");
return 3;
}
//绑定端口号成功,定义一个缓冲区开始接收数据
char buf[1024];
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)
{
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));
}
}
close(socket);
}
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<socket.h>
int main(int argc,int agrv[])
{
if(argc!=3)
{
printf("Usage %s [ip] [port]",argv[1]);
return 1
}
int sock=socket(AF_INET,SOCK_DGRAM,0);
if(sock<0)
{
perror("socket");
return 2
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(argv[2]);
server.sin_addr.s_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),(struct sockaddr*)&server,sizeof(peer));
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);
}
}
}
}
socket的参数使用SOCK_DGRAM表示UDP;
使用sendto()和recvfrom()来接受和发送数据
有关地址转换的函数
字符串转in_addr
int inet_aton(const char *cp, struct in_addr *inp);
//将字符串转化为IP地址
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
//将四进制转化为点分十进制,inet_ntoa这个函数把返回结果放到了静态存储区,不需要手动进行释放,这个函数如果被多次调用,之前调用的结果会被覆盖。在Centos 6.5版本下是不可重入的。
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);