学习socket 编程之前必须先要理解TCP/IP通信的过程
网络通信的实质:
1、解决不同主机进程间的通信.
2、首要解决网络间进程标识问题
3、解决多重协议的识别问题
OSI 和TCP/IP 的联系
OSI TCP/IP
TCP/IP 通信
链路层是设备到设备之间的通信
网络层主机到主机的通信
运输层是进程到进程的通信
应用层是应用程序间的通信
TCP/IP 各层的对应协议
TCP/IP协议族
应用层:应用程序间沟通的层
FTP、Telnet etc.
传输层:提供进程间的数据传送服务
这一层负责传送数据,并且确定数据已被送达并接收。
传输控制协议(TCP),用户数据报协议(UDP)
网络层:提供基本的数据封包传送功能
让每个数据包都能够到达目的主机
IP协议(网际协议)
物理链路层:对实际的网络媒体的管理,定义如何使用实际网络来传送数据
相关的几种协议间的比较:
IP协议
1、网际协议IP是TCP/IP的心脏,也是网络层中最重要的协议
2、IP层接收由更低层(网络接口层例如以太网设备驱动程序)发来的数据包,并把该数据包发送到更高层---TCP或UDP层;
3、相应的,IP层也把从TCP或UDP层接收来的数据包传送到更低层。
4、IP数据包是不可靠的,因为IP并没有做任何事情来确认数据包是按顺序发送的或者没有被破坏
5、IP数据包中含有发送它的主机的地址(源地址)和接收它的主机的地址(目的地址)。
ICMP协议
1、ICMP与IP位于同一层,被用来传送IP的控制信息
2、主要用来提供有关通向目的地址的路径信息
3、ICMP的‘Redirect’信息通知主机通向其他系统的更准确的路径
4、而‘Unreachable’信息则指出路径有问题
5、PING是最常用的基于ICMP的服务
TCP协议
1、TCP协议可以对包进行排序并检查错误,同时实现虚电路间的连接
2、TCP数据包中包括序号和确认
3、未按照顺序收到的包可以被排序,而损坏的包可以被重传
4、面向连接的服务(例如Telnet、FTP、rlogin、X Windows和SMTP)需要高度的可靠性,它们使用TCP来实现
UDP协议
1、UDP与TCP位于同一层
2、不对数据包的顺序进行检查
4、没有错误检测和重传机制
5、不被应用于那些使用虚电路的面向连接的服务
6、主要用于那些面向查询——应答的服务
例如NFS、NTP(网络时间协议)和DNS(域名解析协议)
因此,欺骗UDP包比欺骗TCP包更容易,因为UDP没有建立初始化连接(也可以称为握手),即,与UDP相关的服务面临着更大的危险
数据通信的大体流程
地址和端口
地址:
1、物理地址
以太网内的物理地址是一个48bit的值,网卡生产过程中即固定
修改MAC地址的方法是
# ifconfig eth0 down
# ifconfig eth0 hw ether 00:01:02:03:04:05
# ifconfig eth0 up
2、ip地址
TCP/IP协议使计算机之间进行与底层物理网络无关的通信,可以跨越不同的局域网,甚至不同的物理网络
由32bit组成
{子网ID,主机ID}
子网ID表示由子网掩码覆盖的连续位
主机ID表示以外的位
A类地址:8bit子网ID,第一位为0
B类地址:16bit子网ID,前两位为10
C类地址:24bit子网ID,前三位为110
端口:
1、按照OSI七层协议的描述,传输层和网络层的最大区别是传输层提供进程通信能力
2、网络通信的最终地址不仅是主机地址,还包括可以描述进程的某种标识.
3、TCP/IP协议提出了端口的概念,用于标识通信的进程.
端口最大的意义在于它实现了进程与传输层之间的数据通道和标识,一个进程通过系统调用获取到一个端口后,传输层送到该端口的数据全部被该进程接收,同样,进程送交传输层的数据也通过该端口被送出。
端口号
类似于文件描述符,每个端口都拥有一个叫端口号的整数描述符,以区别不同的端口.
端口号的分配
TCP/IP端口号的分配综合了全局分配和本地分配两种方式,将端口号分成两部分
少量做为保留端口,以全局方式分配给服务进程,因此每个标准服务器都拥有一些全局公认的端口,即使在不同机器上,其端口号也相同。剩余的为自由端口,以本地方式分配.
TCP/UDP规定,小于256的端口才能作为保留端口.
0-1023范围内的端口是预留端口,这些端口只有超级用户才能使用,系统中常见的网络服务使用这些端口
1024-49151是已经注册的端口范围
49152-65535是可以自由使用的端口.
例如:常见端口号
ftp: 21
www: 80
Telnet: 23
网络的服务方式
网络层及其以下各层又称为通信子网,只提供了点到点的通信,没有程序或进程的概念;
传输层提供的是端到端的通信,引入网间进程的概念,同时也要解决差错控制,流量控制,数据排序,连接管理等问题。
进程间通信提供了不同的服务方式:
1、面向连接(虚电路)
2、无连接方式
其中:
面向连接服务
A、电话系统服务模式的抽象
B、每一次完整的数据传输都要经过建立连接、使用连接、终止连接的过程
C、在数据传输过程中,各数据分组不携带目的地址,而使用连接号
D、本质上,连接是一个管道,收发数据不但顺序一致,而且内容相同
E、TCP协议提供面向连接的虚电路
无连接服务
A、邮政系统服务的抽象
B、每个分组都携带完整的目的地址,各分组在系统中独立传送
C、不能保证分组的先后顺序
D、不进行分组出错的恢复和重传
E、不保证传输的可靠性
F、UDP协议提供无连接的数据报服务.
通信作用方式:
客户机/服务器模式
1、服务器方工作过程
1)打开一通信通道并告知本地主机,它愿意在一公认的地址端口(如80)上接收客户请求
2)等待客户请求到达该端口
3)接收客户请求并发送应答信号,激活一新进程处理这个客户请求
4)服务完成后,关闭新进程与客户的通信链路
5)主进程继续等待新的客户请求
6)关闭服务器.
2、客户机方工作过程
1)打开一通信通道并连接到服务器特定端口
2)向服务器发出服务请求,等待并接收应答
3)根据需要继续提出请求
4)请求结束后关闭通信通道并终止
下面正式进到socket 编程
概念:
Socket(套接字)是BSD提供的网络应用编程界面,现在它已经是网络编程中的标准
Socket是一种特殊的进程间通信方式,不同机器上的进程都可以使用这种方式进行通信,网络中的数据传输是一种I/O操作,Socket也是一种文件描述符,它代表了一个通信管道的一个端点。read,write,close操作可应用于Socket描述符,在socket类型的文件描述符上,可以完成建立连接,数据传输等操作。
类型
常用的Socket类型有两种
流式Socket-SOCK_STREAM,提供面向连接的Socket
数据报式Socket-SOCK_DGRAM,提供面向无连接的Socket
socket 编程的大体过程
1、准备工作
主要解决ip地址的存放问题,这遇到一个字节序的问题
2、开始通信
1)创建套接字socket
2)分服务器和客户机端两种不同的情况分别处理
3、数据传输
4、关闭连接
具体如下图
字节序的问题
由于历史的原因,业界存在两种字节序标准:BigEndian(大头、大端)和LittleEndian(小头、小端),Power PC是大头,X86是小头,有些CPU可以通过寄存器设置支持不同的字节序,例如MIPS;
所谓大头就是高位在低字节,低位在高字节;小头则与此相反,以0x345678为例,大头内存从低到高的存放次序为00,34,56,78,小头内存从低到高的存放次序为78,56,34,00;(上面的数值统一为16进制表示形式)
字节序问题广泛存在于设备与设备之间、单板与单板之间、单板与底层芯片之间,只要两个处理单元的字节序不同,这个问题就存在,为了解决不同字节序的处理单元之间的通信问题,业界定义了主机序和网络序的概念,网络序主要用于信息传递,一般不用于计算,其字节顺序与大头一致;
除了在编码时绷紧这根弦以外,我们在器件选择时尽量选择主机序与网络序一致的芯片,同一设备的不同单板使用相同的字节序,并优先选择支持大头的芯片,这样,即使不能彻底解决问题,也可以彻底规避问题。
解决办法:
字节序转换函数
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostint32);
uint16_t htons(uint16_t hostint16);
以上返回网络字节序
uint32_t ntohl(uint32_t netint32);
uint16_t ntohs(uint16_t netint16);
以上返回主机字节序
网络地址的存储形式
因特网地址定义在<netinet/in.h>中.在IPV4因特网域(AF_INET)中,套接字地址用如下结构sockaddr_in表示.
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
sa_family_t sin_family; //协议族
in_port_t sin_port; //端口号
struct in_addr sin_addr; //ip地址
unsigned char sin_zero[8]; //初始化为全 0
};
强制转换地址成通用的地址结构sockaddr
struct sockaddr
{
sa_family_t sa_family;
char sa_data[14];
};
struct in_addr以一个32位无符号数来表示,通常需要用到点分十进制数串与它之间的转换:
int inet_pton(int family, const char *strptr, void *addrptr);
成功返回1,否则返回0
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
len代表strptr缓冲区的长度
#define INET_ADDRSTRLEN 16
创建套接字
创建套接字是进行任何网络通信时必须做的第一步
创建一个用于网络通信的I/O描述符(套接字)
相当于在对文件读写前先用open获取文件描述符
#include <sys/socket.h>
int socket(int family, int type,int protocol);
family:协议族
AF_INET,AF_INET6,AF_LOCAL,AF_ROUTE,AF_KEY
type:套接口类型
SOCK_STREAM,SOCK_DGRAM,SOCK_SEQPACKET,SOCK_RAW
protocol:协议类别
0,IPPROTO_TCP,IPPROTO_UDP,IPPROTO_SCTP
返回值:套接字
创建服务器端
做为服务器需要具备的条件:
1)、具备一个可以确知的地址,以便让别人找到我
2)、使用socket创建一个套接字时,系统不会分配一个理想的端口
3)、让操作系统知道你是一个服务器,而不是一个客户端
4)、使用socket创建的是主动套接字,但是作为服务器,需要被动等待别人的连接
5)、等待连接的到来,对于面向连接的TCP协议来说,连接的建立才真正意味着数据通信的开始
1、使用一个确知的端口来接收客户端的连接
bind函数将一个地址绑定到套接字
#include <sys/socket.h>
int bind(int sockfd,
const struct sockaddr *myaddr,
socklen_t addrlen);
sockfd:socket套接口描述字
myaddr:指向特定于协议的地址结构指针
addrlen:该地址结构的长度
返回值:0,成功;其他,失败
2、让套接字成为被动的
Listen函数可以将套接口由主动修改为被动,操作系统为该套接口设置一个连接队列,来记录所有连接到该套接口的连接
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:socket套接口描述字
backlog:连接队列的长度
返回值:0,成功;其他,失败
3、等待连接的到来
Accept函数从连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待
#include <sys/socket.h>
int accept(int sockfd,
struct sockaddr *cliaddr,
socklen_t *addrlen);
sockfd:socket套接口描述字
cliaddr: 客户端地址
addrlen:客户端地址结构体长度
返回值:已连接的套接口
注:accept函数返回的是一个套接口,这个套接口代表了当前这个连接
创建客户端
作为客户端需要具备的条件:知道服务器的IP地址以及端口号。相对服务器端,它很简单,只需建立连接。
客户端需要主动跟服务器建立连接,连接建立才可以开始传输数据(对于TCP协议)
#include <sys/socket.h>
int connect(int sockfd,
const struct sockaddr *addr,
socklen_t len);
sockfd:socket套接口描述字
addr: 服务器地址
addrlen:服务器地址结构体长度
返回值:0,成功;其他,错误
connect函数建立连接之后不会产生新的套接口
数据通信
当连接建立后,通信的两端便具备了两个套接口
套接口也是一种文件描述符,所以read、write函数可以用于从这个通信管道取出或向其写入数据
发数据:
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf,
size_t nbytes, int flags);
ssize_t sendto(int sockfd, const void
*buf, size_t nbytes, int flags,
const struct sockaddr *destaddr,
socklen_t destlen);
ssize_t sendmsg(int sockfd, const struct
msghdr *msg, int flags);
相应的收数据:
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf,
size_t nbytes, int flags);
ssize_t recvfrom(int sockfd, void
*restrict buf, size_t len, int flags,
struct sockaddr *restrict addr,
socklen_t *restrict addrlen);
ssize_t recvmsg(int sockfd,
struct msghdr *msg, int flags);
关闭连接
使用close函数即可关闭套接字
关闭一个代表已建立连接的套接字将导致另一端接收到一个0长度的数据包
做服务器时,关闭socket创建的套接字将导致服务器无法继续接受新的连接,但不会影响已经建立的连接;关闭accept返回的套接字将导致它所代表的连接被关闭,但不会影响服务器的监听
做客户端时,关闭连接就是关闭连接,不意味着其他
例子:(只建立了服务器端,由windows 的telnet 测试 )
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // bzero
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> // inet_ntop
int main(int argc, char *argv[])
{
char recvbuf[2048]; // 接收缓冲区
int sockfd; // 套接字
struct sockaddr_in servAddr; // 服务器地址结构体
unsigned short port = 8000; // 监听端口
if(argc > 1) // 由参数接收端口
{
port = atoi(argv[1]);
}
printf("TCP Server Started at port %d!/n", port);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 创建TCP套接字
if(sockfd < 0)
{
perror("Invalid socket");
exit(1);
}
bzero(&servAddr, sizeof(servAddr)); // 初始化服务器地址
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(port);
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
printf("Binding server to port %d/n", port);
if(bind(sockfd, (struct sockaddr*)&servAddr, sizeof(struct sockaddr)) != 0)
{
close(sockfd);
perror("binding err!");
exit(1);
}
if(listen(sockfd, 1) != 0)
{
close(sockfd);
perror("listen err!");
exit(1);
}
printf("waiting client.../n");
while(1)
{
char cliIP[INET_ADDRSTRLEN]; // 用于保存客户端IP地址
size_t recvLen;
struct sockaddr_in cliAddr; // 用于保存客户端地址
size_t cliAddrLen = sizeof(cliAddr);
// 必须初始化!!!
int connfd = accept(sockfd, (struct sockaddr*)&cliAddr, &cliAddrLen);
// 获得一个已经建立的连接
if(connfd < 0)
{
close(sockfd);
perror("accept err!");
exit(1);
}
inet_ntop(AF_INET, &cliAddr.sin_addr.s_addr, cliIP, INET_ADDRSTRLEN);
printf("client ip = %s/n", cliIP);
while((recvLen = read(connfd, recvbuf, 2048)) > 0)
{
write(connfd, recvbuf, recvLen);
}
close(connfd);
printf("client closed!/n");
}
close(sockfd);
return 0;
}