Linux 网络编程
计算机网络体系结构模式
所有的网络通信方式分为两种:线路交换和包交换。
所谓的线路交换,就是指再传输时在发送端和接收端之间建立一个特定的线路连接,数据
可以在这条线路上传输。电话是采用的这种方式。
计算机网络则采用的是包交换,数据的发送端将要传输的数据分割成块,而每个块经过适
的处理后形成一个数据包,包中有接收端的地址等必要信息,每个包单独传输。包中的数据
不是限定死的,只要保证数据的正确传输即可,具体应该定义哪些信息,则与使用的协议有
。
OSI 标准
OSI 标准是开放系统互联标准( Open Systerm Interconnection )即我们通常所说的网络 互
的七层框架,他是 1977 年国际标准化组织提出的一种参考模型。值得注意的是, OSI 并没有
供一个可以实现的方法,它不是一个标准而只是一个制定标准时使用的概念性的框架,更不
一个网络协议。
1 、物理层( Physical Layer ):主要功能为定义了网络的物理结构,传输的电磁标准, Bi t
流的编码及网络的时间原则,如分时复用及分频复用。决定了网络连接类型(端到端或多端连
接)及物理拓扑结构。说的通俗一些,这一层主要负责实际的信号传输。
2 、链路层( Data Link Review ):在两个主机上建立数据链路连接,向物理层传输数据信
号,并对信号进行处理使之无差错并合理的传输。
3 、网络层( Network Layer ):主要负责路由,选择合适的路径,进行阻塞控制等功能。
4 、传输层( Transfer Layer ):最关键的一层,向拥护提供可靠的端到端( End-to-End )
服务,它屏蔽了下层的数据通信细节,让用户及应用程序不需要考虑实际的通信方法。
5 、会话层( Session Layer ):主要负责两个会话进程之间的通信,即两个会话层实体之 间
的信息交换,管理数据的交换。
6 、表示层( Presentation Layer ):处理通信信号的表示方法,进行不同的格式之间的翻
译,并负责数据的加密解密,数据的压缩与恢复。
7 、应用层( Application Layer ):保持应用程序之间建立连接所需要的数据记录,为用 户
服务。
在工作中,每一层会给上一层传输来的数据加上一个信息头( header ),然后向下层发出,
然后通过物理介质传输到对方主机,对方主机每一层再对数据进行处理,把信息头取掉,最后
还原成实际的数据。本质上,主机的通信是层与层之间的通信,而在物理上是从上向下最后通
过物理信道到对方主机再从下向上传输。
TCP/IP 协议
在实际应用中,最重要的是 TCP/IP ( Transport Control Protocol/Internet Protocol )
协议,它是目前最流行的商业化的协议,相对于 OSI ,它是当前的工业标准或 “ 事实的标准 ” ,
在 1974 年由 Kahn 提出的。它分为四个层次(从高到低):应用层(与 OSI 的应用层对应),传
输层(与 OSI 的传输层对应),互联层(与 OSI 的网络层对应),主机 - 网络层(与 OSI 的数据 链
路层和物理层对应)。
1. 应用层
应用层包括网络应用程序和网络进程,是与用户交互的界面,他为用户提供所需要的各种
服务,包括远程登陆、文件传输和电子邮件等。他的作用相当于 OSI 中的应用层及表示层和会
话层。
2. 传输层
相当于 OSI 中的传输层,他为应用程序提供通信服务,这种通信又叫端对端通信。他有三
个主要协议:传输控制协议( TCP ),用户数据包协议( UDP ),和互联网控制消息协议( ICMP )。
TCP 协议:以建立连接高可靠性的传输为目的,他负责把大量的用户数据按一定的长度组 成
多个数据包进行发送,并在接受到数据包之后按分解顺序重组和恢复用户数据。他是一种面向
连接的可靠的双向通信的数据流。
UDP 协议:提供无连接数据包传输服务,他把用户数据分解为多个数据包后发送给接收方。
它具有执行代码小,系统开销小和处理速度快等优点。
ICMP 协议:主要用于端主机和网关以及互联网管理中心等地消息通信,以达到控制管理网络运
行的目的。 ICMP 协议能发送出错消息给发送数据包的端主机,还有限制流量的功能。
3. 网络层
相当于 osi 的网络层,使用的协议是 ip 协议。他是用来处理机器之间的通信问题,他接 受
输层请求,传输某个具有目的地址信息的分组。该层把分组封装到 ip 数据包中,填入数据 包
头部(包头),使用路由算法来选择是直接把数据包发送到目标主机还是发送给路由器,然 后
数据包交给下面的网络接口层中的对应网络接口模块。
4. 网络接口层
相当于 osi 中的数据连接层和物理层。他负责接收 ip 数据包和把数据包通过选定的网络 发
出去 , 如图
基于 TCP 协议的客户机 / 服务器进程图
l Socket 编程相关函数
socket , bind , listen , accept , connect ,inet_aton , inet_ntoa, htons, htonl ,ntohs, ntohl
( 1 ) socket( 建立连接 )
表头文件 #include<sys/socket.h>
定义函数: int socket(int family,int type,intprotocol);
函数说明: socket() 函数用来生成一个套接口描述字,也称为套接字,指定协议族和套接
口。
参数: family 指定协议族, type 指明字节流方式,而 protocol 一般为 0
Family 的取值范围:
AF_LOCAL UNIX 协议族
AF_ROUTE 路由套接口
AF_INET IPv4 协议
AF_INET6 IPv6 协议
AF_KEY 密钥套接口
参数 type 的取值范围:
SOCK_STREAM TCP 套接口
SOCK_DGRAM UDP 套接口
SOCK_PACKET 支持数据链路访问
SOCK_RAM 原始套接口
返回值:成功返回非负描述字,失败返回负值
( 2 ) bind (对 socket 定位)
表头文件 #include<sys/types.h>
#include<sys/socket.h>
定义函数 int bind(int sockfd,struct sockaddr * my_addr,int addrlen);
函数说明 bind() 用来设置给参数 sockfd 的 socket 一个名称。此名称由参数 my_addr 指
向一 sockaddr 结构,对于不同的 socket domain 定义了一个通用的数据结构
struct sockaddr
{
unsigned short int sa_family;
char sa_data[14];
};
sa_family 为调用 socket ()时的 domain 参数,即 AF_xxxx 值。
sa_data 最多使用 14 个字符长度。
此 sockaddr 结构会因使用不同的 socket domain 而有不同结构定义,例如使用 AF_INET
domain ,其 socketaddr 结构定义便为
struct socketaddr_in
{
unsigned short int sin_family;
uint16_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct in_addr
{
uint32_t s_addr;
};
sin_family 即为 sa_family
sin_port 为使用的 port 编号
sin_addr.s_addr 为 IP 地址
sin_zero 未使用。
参数 socket 为套接字, my_addr 是一个指向特定协议地址结构的指针, addrlen 为
sockaddr 的结构长度。
返回值 成功则返回 0 ,失败返回 -1 ,错误原因存于 errno 中。
错误代码
EBADF 参数 sockfd 非合法 socket 处理代码。
EACCESS 权限不足
ENOTSOCK 参数 sockfd 为一文件描述词,非 socket 。
( 3 ) listen (等待连接)
相关函数 socket , bind , accept , connect
表头文件 #include<sys/socket.h>
定义函数 int listen(int s,int backlog);
函数说明 listen() 用来等待参数 s 的 socket 连线。参数 backlog 指定同时能处理的最 大
连接要求,如果连接数目达此上限则 client 端将收到 ECONNREFUSED 的错误。 Listen() 并未开
始接收连线,只是设置 socket 为 listen 模式,真正接收 client 端连线的是 accept() 。通常
listen() 会在 socket() , bind() 之后调用,接着才调用 accept() 。
返回值 成功则返回 0 ,失败返回 -1 ,错误原因存于 errno
附加说明 listen() 只适用 SOCK_STREAM 或 SOCK_SEQPACKET 的 socket 类型。如果 socket
为 AF_INET 则参数 backlog 最大值可设至 128 。
错误代码
EBADF 参数 sockfd 非合法 socket 处理代码
EACCESS 权限不足
EOPNOTSUPP 指定的 socket 并未支援 listen 模式。
( 4 ) accept
表头文件 #include<sys/types.h>
#include<sys/socket.h>
定义函数 int accept(int s,struct sockaddr * addr,int * addrlen);
函数说明 accept() 用来接受参数 s 的 socket 连线。参数 s 的 socket 必需先经 bind() 、
isten() 函数处理过,当有连线进来时 accept() 会返回一个新的 socket 处理代码,往后的数 据
传送与读取就是经由新的 socket 处理,而原来参数 s 的 socket 能继续使用 accept() 来接受新
连线要求。连线成功时,参数 addr 所指的结构会被系统填入远程主机的地址数据,参数 add rlen
为 scokaddr 的结构长度。关于结构 sockaddr 的定义请参考 bind() 。
返回值 成功则返回新的 socket 处理代码,失败返回 -1 ,错误原因存于 errno 中。
错误代码 EBADF 参数 s 非合法 socket 处理代码。
EFAULT 参数 addr 指针指向无法存取的内存空间。
ENOTSOCK 参数 s 为一文件描述词,非 socket 。
EOPNOTSUPP 指定的 socket 并非 SOCK_STREAM 。
EPERM 防火墙拒绝此连线。
ENOBUFS 系统的缓冲内存不足。
ENOMEM 核心内存不足。
( 5 ) connect (建立 socket 连线)
相关函数 socket , bind , listen
表头文件 #include<sys/types.h>
#include<sys/socket.h>
定义函数 int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
函数说明 connect() 用来将参数 sockfd 的 socket 连至参数 serv_addr 指定的网络地
址。结构 sockaddr 请参考 bind() 。参数 addrlen 为 sockaddr 的结构长度。
返回值 成功则返回 0 ,失败返回 -1 ,错误原因存于 errno 中。
错误代码 EBADF 参数 sockfd 非合法 socket 处理代码
EFAULT 参数 serv_addr 指针指向无法存取的内存空间
ENOTSOCK 参数 sockfd 为一文件描述词,非 socket 。
EISCONN 参数 sockfd 的 socket 已是连线状态
ECONNREFUSED 连线要求被 server 端拒绝。
ETIMEDOUT 企图连线的操作超过限定时间仍未有响应。
ENETUNREACH 无法传送数据包至指定的主机。
EAFNOSUPPORT sockaddr 结构的 sa_family 不正确。
EALREADY socket 为不可阻断且先前的连线操作还未完成。
( 6 ) send() 和 recv() 这两个函数用于面向连接的 socket 上进行数据传输。
函数定义: int send(int sockfd, const void *msg, int len, int flags);
参数: Sockfd 是你想用来传输数据的 socket 描述符; msg 是一个指向要发送数据的指针; Len
是以字节为单位的数据的长度; flags 一般情况下置为 0 (关于该参数的用法可参照 man 手册) 。
返回值: send() 函数返回实际上发送出的字节数,可能会少于你希望发送的数据。在程序
中应该将 send() 的返回值与欲发送的字节数进行比较。当 send() 返回值与 len 不匹配时,应 该
对这种情况进行处理。
char *msg = "Hello!";
int len, bytes_sent;
……
len = strlen(msg);
bytes_sent = send(sockfd, msg,len,0);
……
函数定义: int recv(int sockfd,void *buf,int len,unsigned int flags);
参数: sockfd 是接受数据的 socket 描述符; buf 是存放接收数据的缓冲区; len 是缓冲 的
长度。 Flags 也被置为 0 。
返回值: Recv() 返回实际上接收的字节数,当出现错误时,返回 -1 并置相应的 errno 值。
sendto() 和 recvfrom() 用于在无连接的数据报 socket 方式下进行数据传输。由于本地
socket 并没有与远端机器建立连接,所以在发送数据时应指明目的地址。
(七) inet_aton 地址变换
Int inet_aton(const char *cp, struct in_addr *inp);
Char *inet_ntoa(struct in_addr in);
inet_aton把xxx.xxx.xxx.xxx形式转换成32位的IP,存储在INP指针所指向的地方,inet_ntoa则是把32位IP地址转换成XXX.XXX.XXX.XXX的格式。
(八) htons htonl ntohs ntohl字节序转换
Htons 把unsigned short 类型从主机序转换到网络序
Htonl把unsigned long 类型从主机序转换到网络序
Ntohs 把unsigned short 类型从网络序转换到主机序
Ntohl 把unsigned long 类型从网络序转换到主机序
实例一:
// 描述:// 程序名称: server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <sys/ioctl.h>
#include <termios.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
void main()
{
pid_t fd;
int listensock,connsock;
char recvbuff[100]; // 定义要接收的数据缓冲区
struct sockaddr_in serveraddr; // 定义网络套接字地址结构
listensock = socket(AF_INET,SOCK_STREAM,0); // 创建一个套接字,用于监听
bzero(&serveraddr,sizeof(struct sockaddr)); // 地址结构清零
serveraddr.sin_family = AF_INET; // 指定使用的通讯协议族
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 指定接受任何连接
serveraddr.sin_port = htons(5000); // 指定监听的端口
bind(listensock,(struct sockaddr *)&serveraddr,sizeof(structsockaddr_i
// 给套接口邦定地址
listen(listensock,1024); // 开始监听
connsock = accept(listensock,(struct sockaddr *)NULL, NULL);
// 建立通讯的套接字, accept 函数,等待客户端程序使用 connect 函数的连接
recv(connsock,recvbuff,sizeof(recvbuff),0); // 接收服务器的数据
printf("%s\n",recvbuff); // 打印接收到的数据
sleep(2);
close(connsock); // 关闭通讯套接字
close(listensock); // 关闭监听套接字
}
// 描 述: 客户端请求连接,成功后发走一段数据
// 程序名称: client.c
// 头文件同上
int main(int argc,char **argv)
{
pid_t fd;
// 定义要发送的数据缓冲区;
const char buff[] = "Hello! World!\r\n";
int sockfd,connsock; // 定义一个 socket 套接字,用于通讯
struct sockaddr_in serveraddr; // 定义网络套接字地址结构
if(argc!= 2)
{
printf("Usage: echo ip 地址 ");
exit(0);
}
sockfd =socket(AF_INET,SOCK_STREAM,0); // 创建一个套接字
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET; // 指定使用的通讯协议族
serveraddr.sin_port = htons(5000); // 指定要连接的服务器的端口
inet_pton(AF_INET, argv[1], &serveraddr.sin_addr);
// 连接服务器
connect(sockfd,(struct sockaddr *)&serveraddr,sizeof(serveraddr));
send(sockfd,buff,sizeof(buff), 0); // 向客户端发送数据
close(sockfd); // 关闭套接字
return(0);
}
实例二:
//tcp_server.c
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define portnumber 3333
int main(int argc, char *argv[])
{
int sockfd,new_fd;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
int sin_size;
int nbytes;
char buffer[1024];
/* 服务器端开始建立sockfd描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) // AF_INET:IPV4;SOCK_STREAM:TCP
{
fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
exit(1);
}
/* 服务器端填充 sockaddr结构 */
bzero(&server_addr,sizeof(struct sockaddr_in)); // 初始化,置0
server_addr.sin_family=AF_INET; // Internet
server_addr.sin_addr.s_addr=htonl(INADDR_ANY); // (将本机器上的long数据转化为网络上的long数据)和任何主机通信 //INADDR_ANY 表示可以接收任意IP地址的数据,即绑定到所有的IP
//server_addr.sin_addr.s_addr=inet_addr("192.168.1.1"); //用于绑定到一个固定IP,inet_addr用于把数字加格式的ip转化为整形ip
server_addr.sin_port=htons(portnumber); // (将本机器上的short数据转化为网络上的short数据)端口号
/* 捆绑sockfd描述符到IP地址 */
if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
{
fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
exit(1);
}
/* 设置允许连接的最大客户端数 */
if(listen(sockfd,5)==-1) //使用最大连接数是5个连接
{
fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
exit(1);
}
while(1)
{
/* 服务器阻塞,直到客户程序建立连接 */
sin_size=sizeof(struct sockaddr_in);
if((new_fd=accept(sockfd,(struct sockaddr *)(&client_addr),&sin_size))==-1)
{
fprintf(stderr,"Accept error:%s\n\a",strerror(errno));
exit(1);
}
fprintf(stderr,"Server get connection from %s\n",inet_ntoa(client_addr.sin_addr)); // 将网络地址转换成.字符串
if((nbytes=read(new_fd,buffer,1024))==-1)
{
fprintf(stderr,"Read Error:%s\n",strerror(errno));
exit(1);
}
buffer[nbytes]='\0';
printf("Server received %s\n",buffer);
/* 这个通讯已经结束 */
close(new_fd);
/* 循环下一个 */
}
/* 结束通讯 */
close(sockfd);
exit(0);
}
//tcp_client.c
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define portnumber 3333
int main(int argc, char *argv[])
{
int sockfd;
char buffer[1024];
struct sockaddr_in server_addr;
struct hostent *host;
/* 使用hostname查询host 名字 */
if(argc!=2)
{
fprintf(stderr,"Usage:%s hostname \a\n",argv[0]);
exit(1);
}
if((host=gethostbyname(argv[1]))==NULL) //gethostbyname函数返回hostent结构体的信息
{
fprintf(stderr,"Gethostname error\n");
exit(1);
}
/* 客户程序开始建立 sockfd描述符 */
if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1) // AF_INET:Internet;SOCK_STREAM:TCP
{
fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
exit(1);
}
/* 客户程序填充服务端的资料 */
bzero(&server_addr,sizeof(server_addr)); // 初始化,置0
server_addr.sin_family=AF_INET; // IPV4
server_addr.sin_port=htons(portnumber); // (将本机器上的short数据转化为网络上的short数据)端口号
server_addr.sin_addr=*((struct in_addr *)host->h_addr); // IP地址
/* 客户程序发起连接请求 */
if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
{
fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));
exit(1);
}
/* 连接成功了 */
printf("Please input char:\n");
/* 发送数据 */
fgets(buffer,1024,stdin);
write(sockfd,buffer,strlen(buffer));
/* 结束通讯 */
close(sockfd);
exit(0);
}