网络编程就是编写程序使两台计算机能交互数据。读者需要对操作系统和TCP/IP协议具有一定的基础。TCP/IP协议相关知识可以参考文章(TCP/IP连接)。
一、socket原理
学过TCP/IP网络编程惹读者肯定知道socket被称为套接字,那么socket在网络编程中的作用是什么呢?
1、套接字在网络编程中的作用是什么?
在TCP/IP协议栈中,在网络层IP地址可以代表唯一的一台主机,但是实际上网络通信是主机应用程序之间的通信,一个主机可能有很多进程并发执行,因此还需要端口号来区分进程。故,网络中的进程可以用IP+端口号+协议进行唯一标识。
socket是应用层和传输层的一种抽象层,是应用层与传输层的接口,是支持TCP网络通信的基本通信单元,完成了不同进程间的通信。在互联网中,通信模式是客户端(client)与服务器(server)的端点对端点的通信模式。标识每个端点IP地址和端口号称为套接字。socket={IP:PORT}。标识源IP地址,源port,目的IP 地址,目的port,协议统称为套接字对。
在读相关文章的时候,一种观点着实令人费解:
多个TCP连接或多个应用程序进程可能需要通过同一个TCP协议端口来传输数据。(感觉观点有点误导人)
在这我的理解是一般来说多个应用程序是不可能使用一个端口的,但是多个进程是有可能共用一个端口。多个应用程序应该代表的应该是多个客户端,同一TCP协议端口代表的是服务器端。(如果理解不正确还请大家及时指正,共同学习!)
下面让我们学习一下TCP端口号和并发服务器(http://book.51cto.com/art/201005/203019.htm),加深对此观点的理解。
2、TCP端口号和并发服务器
并发服务器中主服务器循环通过派生一个子进程来处理每个新的连接。如果一个子进程继续使用服务器众所周知的端口来服务一个长时间的请求,那将发生什么?让我们来看一个典型的序列。首先,在主机freebsd上启动服务器,该主机是多宿的(此处理解为具有多个NIC),其IP地址为12.106.32.254和192.168.42.1。服务器在它的众所周知的端口(本例为21)上执行被动打开,从而开始等待客户的请求,如图1所示。
图1 监听套接字
稍后在IP地址为206.168.112.219的主机上启动第一个客户,它对服务器的IP地址之一12.106.32.254执行主动打开。我们假设本例中客户主机的TCP为此选择的临时端口为1500,如图2所示。图中在该客户的下方标出了它的套接字对。
图 2 客户端连接请求
当服务器接收并接受这个客户的连接时,它fork一个自身的副本,让子进程来处理该客户的请求,如图3所示。
至此,我们必须在服务器主机上区分监听套接字和已连接套接字(connected socket)。注意已连接套接字使用与监听套接字相同的本地端口(21)。还要注意在多宿服务器主机上,连接一旦建立,已连接套接字的本地地址(12.106.32.254)随即填入。
图 3 并发服务器让子进程处理客户
下一步我们假设在客户主机上另有一个客户请求连接到同一个服务器。客户主机的TCP为这个新客户的套接字分配一个未使用的临时端口,譬如说1501,如图4所示。服务器上这两个连接是有区别的:第一个连接的套接字对和第二个连接的套接字对不一样,因为客户的TCP给第二个连接选择了一个未使用的端口(1501)。
TCP无法仅仅通过查看目的端口号来分离外来的分节到不同的端点。它必须查看套接字对的所有4个元素才能确定由哪个端点接收某个到达的分节。图2-14中对于同一个本地端口(21)存在3个套接字。如果一个分节来自206.168.112.219端口1500,目的地为12.106.32.254端口21,它就被递送给第一个子进程。如果一个分节来自206.168.112.219端口1501,目的地为12.106.32.254端口21,它就被递送给第二个子进程。所有目的端口为21的其他TCP分节都被递送给拥有监听套接字的最初那个服务器(父进程)。
通过这个例子,我们可以更加深刻的理解socket的作用。应用层可以和传输层通过socket接口,区分来着不同客户端的通信,实现数据传输的并发服务。
二、套接字的编程要点
创建套接字的函数是(Linux 版本):
#include<sys/socket.h>
int socket(int domain, int type, int protocol );
参数解释:domain----- 套接字中使用的协议族信息
type---------套接字数据传输类型信息
protocol----计算机通信中所使用的协议信息
1、协议族(Protocol Family)
PF_INET IPV4互联网协议族
PF_INET6 IPV6互联网协议族
PF_LOCAL 本地通信的UNIX协议族
2、套接字的类型----数据传输的类型
(1) 面向连接的套接字(SOCK_STREAM)
特点: 1) 传输过程中数据不会丢失
2) 按序传输数据
3) 传输数据不存在边界。 在IPV4协议族中能满足面向连接的套接字协议类型只有TCP协议
(2)面向消息的套接字(SOCK_DGRAM)
特点:强调快速传输而非传输顺序;传输数据可能丢失,传输不可靠;传输数据有边界;限制每次传输大小;
在IPV4协议族中,面向消息的套接字称为UDP套接字。
关于数据边界的相关知识点,可以参考 sock数据边界说明。
三、地址族与数据序列
1、相关知识点储备
1) IPV4地址族表示,包含了IP地址和端口号信息
struct sockaddr_in{
sa_family_t sin_family; //地址族(Address_Family ) PF_INET--IPV4地址族 PF_INET6--IPV6地址族
struct in_addr sin_addr; //32位IP 地址(二进制表示,以网络字节序保存)
uint_16 sin_port; // 16位port ,以网络字节序保存 IP+port占用了4+2=6字节
char size_zero[8]; //不使用,但是具有重要的作用。struct sockaddr{}介绍。
}
struct in_addr {
in_addr_t s_addr; //32位IPV4地址
}
在struct sock_addr 中,32位ip地址和16位port地址是以网络字节序保存的,那么什么是网络字节序?如何转换为网络字节序?
2)网络字节序与地址转换
CPU向内存保存数据的方式有两种:
大端序:高位字节存放在低位地址。Intel系列的CPU是大端序存储
小端序:高位字节存放在高位地址。故在通过互联网传输数据时,必须要采用统一的格式---大端序
字节序转换函数:
unsigned short htons(unsigned short); //把short 类型的数据从主机字节转换为网络字节
unsigned short ntohs(unsigned short); //把short 类型的数据从网络字节转换为主机字节
unsigned long htonl(unsigned long); //把long 类型的数据从主机字节转换为网络字节
unsigned long ntohl(unsigned long); //把long 类型的数据从网络字节转换为主机字节
具体实现代码如下:(Linux 版本)
3)将IP地址(char类型)转换为网络字节序
在TCP/IP协议中,将网络地址分类:
A类地址的首字节范围:0-127 首位是0
B类地址的首字节范围:128-191 首位是10
C类地址的首字节范围:192-223 首位是110
如何将一个IP地址转换为32位大端序存储的数据? IP:1.2.3.251
使用函数 : in_addr_t inet_addr(const char* string );//将点分十进制格式的字符串转换为32位正兴数据。
inet_addr()函数可以检测无效的IP地址!
使用函数:int inet_aton(const char* string , struct in_addr * addr )
4)向套接字分配网络地址
向套接字分配网络地址采用函数:int bind(int sockfd, struct sockaddr* myaddr,socklen_t addrlen);
参数:成功时返回1,失败时返回0;
sockfd:要分配的地址信息(ip和port)的套接字文件描述符;
myaddr:存有地址信息的结构体变量地址值。
struct sockaddr{
sa_family_t sin_family; // 协议族信息
char sa_data[14]; //地址信息 包含IP地址和端口号,剩余部分填0!这就可以说明为什 // 么在sockaddr_in存在 char size_zero[8] ,用来对齐内存
}
用新的结构体struct sockaddr_in 来写入地址信息,转换为sockaddr型的结构体变量,在传给bind函数。
对于此部分的实现其实Linux和Windows的实现几乎是一样的,那么我么就看一下Windows版本的实现过程,来结束本帖。后续内容持续更新。