目录
1 简要说说网络编程
1.1 不同地域主机上的进程的通信
从标题可以看出,网络也是一种进程间通信的方式。只不过不同的进程运行在不同的计算机上。不同主机要想通信,首先就是如何标识不同的主机?就是通过IP地址,定位到想要通信的主机后,一个主机可能有很多个进程,如何区别不同的进程?就是通过端口号。这样我们就可以实现世界上任何的主机通信了。
说起来简单,计算机网络可不是这么简单就实现了的。实现计算机网络,需要考虑不同通信介质的选取以及其差异,如何保证信号如何在介质上有效的传输(信号传输过程中会衰减)?以及发送数据之前如何确保接收方已经做好了准备?数据传输发生错误了怎么办?等等很多的问题,需要很多的算法和设备的支持才可以的。好在现在所有的这些被互联网提供商实现了,下面我们来大致看看:
1.2 层次化的网络协议
有同学会问,什么是协议?比如,就业协议,合作协议等,这些协议在生活中是写在纸上的,让人们看的,可以知道自己在就业中与公司之间,自己需要履行哪些义务,承担哪些责任,企业要给我们多少工资等,把协议双方要做的事情都写好,是一个规则,双方都要遵守的。
同样,网络协议是写给那些提供网络服务,使用网络服务的机器
看的(包含运行商的交换机,路由器,以及使用网络的主机),目的是让参与网络数据交换的计算机遵守的。当然,机器是看不懂写在纸上的文字的,如何让机器看懂——程序。把协议规定的动作编写成程序,让机器运行即可。具体协议规定了什么,这里不做深究,可以去参考相关书籍。现在我们来简单看看协议的层次。
协议为什么要分层,因为实现计算机网络的细节太多了,不可能一下子讲的完,也讲不清楚;分层在于简化细节,把负责同一块功能的,或相似功能的规定放在一个层面上讨论,思路更清晰。那么都分为哪几层呢?
- 首先,网络都需要那些物理介质,电缆,电缆之间如何连接,电缆有哪些引脚,电平是如何变化… … ,规定这些细节的是物理层的协议
- 其次,数据信号如何在电缆上传输?,如何区分不同数据之间的边界?信号差错如何控制?… …规定这些细节的是数据链路层的协议
- 再者,数据从起点开启,到终点主机,需要经过很多路由器,来把不同网络间的数据进行转发,数据如何从一个路由器到下一个路由器?如何做能够保证网络的最大稳定性?如何评估单点路由故障?规定这些细节的是网络层的协议
- 然后,如何保证送到目的主机的数据送到指定进程,如何为进程提供一个可靠的数据服务?如何保证不同数据的顺序,当数据丢失重新传输需要遵守哪些规则,当网络数据量大时,如何动态的控制流量?规定这些细节的是运输层的协议
- 最后,数据送到指定进程之后,如何使用这些数据?用做图片?视频?文档?还是其他内容,规定这些细节的是应用层的协议。
1.3 网络协议的实现位置
什么是协议的实现位置?确切的说是这几层协议的程序是运行在哪里?用户有计算机,运营商有很多网络设备,怎么分布呢?
- 首先,物理层的协议是网络设备设计商,制造商实现的,我们不用管
- 其次,数据链路层协议是由局域网提供商实现的,用户电脑的网卡里面的固件存储了数据链路层协议的算法程序;
- 然后,网络层的协议是运营商的路由器执行的,实现在路由器上;
- 运输层协议就与确定主机的进程相关,用户的进程是跑在用户的电脑上的,所以运输层协议实现在用户的计算机上,确切的说是操作系统实现的;
- 应用层协议,是由具体的应用软件实现的,当然是在用户的计算机上。如:邮件应用实现邮件的相关协议(POP STMP),浏览器实现HTTP,HTTPS协议等。
这么多协议的实现,程序员能插手的,也就是运输层协议和应用层协议了,应用层协议与具体用途相关,种类繁多。下面我们来看看运输层协议。
1.4 Linux套接字
运输层的协议分为TCP和UDP协议,这两个协议是操作系统为用户实现的,并提供了一套叫做系统调用
的东西供用户调用,就是 套接字(socket)
,套接字是通过操作系统为我们提供的运输层协议的软件接口,通过这个接口,我们可以使用运输层协议的数据服务。而在Linux操作系统上套接字是通过一些C语言的库函数来提供。
由于Linux系统一切皆文件
设计哲学的影响,socket也被设计成文件,通过一个描述符来引用它。Linux下服务器建立的过程是socket—>bind—>listen—>accept—>recv/send,下面我们来简单说说这些函数。
2 Linux下的C语言套接字API函数
2.1 socket函数
函数原型如下:
int socket(int address_family, int socket_type, int protocol);
该函数有以下要点:
- 使用该函数需要包含
<sys/socket.h>
头文件 - 返回值是一个
int
类型的socket描述符,用于引用/dev/socket
下面的socket文件,一般大于21,返回-1表示出错
- 参数
- address_family是描述一个协议族,指定了网络层的协议,一个网络层协议可以支持多个运输层协议,所以称为协议族,常见的几个
- AF_INET 表示IPv4地址
- AF_INET6 表示IPv6地址
- AF_LOCAL UNIX 协议域
其中AF_前缀可以换成PF_前缀
- socket_type是套接字类型,套接字类型需要和协议族匹配才可以使用,常见的有
- SOCK_STREAM 流式套接字 采用TCP协议
- SOCK_DGRAM 数据报套接字 采用UDP协议
- SOCK_SEQPACKET 有序分组套接字
- SOCK_RAW 原始套接字,可以访问协议栈各层的数据,功能强大,使用复杂
- protocol 使用的传输协议,不同的协议族支持的协议不一样,而每一个协议族有一个默认的协议,可以在此参数赋值0来使用,常用的有
- IPPROTO_TCP
TCP协议
- IPPTOTO_UDP
UDP协议
- IPPROTO_SCTP
SCTP协议
- IPPROTO_TCP
- address_family是描述一个协议族,指定了网络层的协议,一个网络层协议可以支持多个运输层协议,所以称为协议族,常见的几个
有了前两个参数,目前我们就可以指导使用的协议是什么,为什么还需要第三个参数呢,是因为以后当通过前两个参数推演不出第三个参数时(比如另一种协议替代了现在的TCP/IP),即有多个协议可选的时候,可以通过第三个参数来区分。当我们通过前两个参数就可以推出第三个的时候,第三个参数可以写0,socket函数的作用是指定了通信的协议
例如:
int tcp_socket = socket(AF_INET, SOCK_STREAM, 0); //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0); //创建UDP套接字
2.2 bind函数
函数原型如下:
int bind(int sockfd, struct sockaddr* addr, socklen_t addrlen)
要点如下:
- 使用该函数需要包含
<sys/socket.h>
头文件 - 返回值是
int
类型,0表示成功,-1标志失败 - 参数
- sockfd socket函数返回的socket描述符
- addr 指向socket的地址的指针
- addrlen socket的地址的大小,socklen_t是int类型的
根据计算机网络中的知识,socket是属于运输层的内容,提供端到端的服务。所以数据在传输的时候,需要指定地址和端口。地址标识了哪台主机,端口标识了主机上的哪个进程。bind函数的作用是把一个socket绑定到一个地址上
,就是说服务器以后一直使用指定的地址和端口了,这样做对于服务器来说是必要的2。对于客户端可以不用绑定地址,客户端在请求服务器的时候,系统会默认使用本机的地址和一个随机的端口,因为服务器一般不会主动找客户端的。
下面讲一下sockaddr
,他是个通用的结构体。定义如下:
struct sockaddr
{
//sa表示 socket address 套接字地址
unsigned short sa_family;//套接字地址族 2字节 与socket函数的address_family参数意思一样
char sa_data[14];//套接字地址数据部分,包含地址,端口号
};
为什么说这是一个通用的结构体呢?因为他只是说明了需要一个套接字地址,但是没有指定地址的格式,如地址几个字节,端口几个字节等,而是提供了一个sa_data数组来保存。这样定义的好处让具体的协议自己来定义格式,有利于程序的扩展。unix的提供机制而不是策略
的设计哲学得以体现。现在最大的互联网是Internet,我们一般使用internet给出的sockaddr格式,定义如下:
struct sockaddr_in//_in 表示这是internet的格式
{
//sin前缀表示 socket internet
unsigned short sin_family;//套接字地址族 2字节
unsigned short sin_port;//端口号 2字节
struct in_addr sin_addr;//IP地址字段 占4个字节,往下看
char sin_zero[8];//剩余的填充0
};
in_addr 这个结构体定义很简单,如下:
struct in_addr
{
unsigned long s_addr;//long类型 4字节
};
其实sockaddr
和sockaddr_in
的在源代码中的定义不是上面的样式,上面的是编译预处理之后的样子,源码中sockaddr的定义也不难理解,位置在bits/socket.h
:
struct sockaddr
{
__SOCKADDR_COMMON (sa_);//是一个预定义宏
char sa_data[14];
};
这个宏在bits/sockaddr.h中
,如下所示:
//也就是把sa_和family拼接在一起,作为一个标识符,然后以这个名字定义变量。
#define __SOCKADDR_COMMON(sa_prefix) \
sa_family_t sa_prefix##family
sa_family_t就是一个短整型:
typedef unsigned short int sa_family_t;
sockaddr_in
的定义在netinet/in.h
中:
typedef uint32_t in_addr_t;
struct in_addr
{
in_addr_t s_addr;//32位的int型表示IP地址
};
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);//sin_family
in_port_t sin_port; //这个也是宏定义
struct in_addr sin_addr;
//计算剩余长度,填充0
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
//__SOCKADDR_COMMON_SIZE 的定义如下:
//#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int))
在这里需要用到IP地址和端口号,请注意网络中使用的IP地址和端口号是大端方式3,而我们使用的机器有可能是小端方式,所以要进行转换,以下有几个转换函数,需要包含头文件arpa/inet.h
:
- htonl (long类型的值)–主机到网络的long类型:将32位整数从主机的字节顺序转换成网络字节顺序。
- htons(short类型的值)–主机到网络的short类型:将16位整数从主机的字节顺序转换成网络字节顺序。
- ntohl(long类型的值)–网络到主机的long类型:将32位整数从网络字节顺序转换成主机的字节顺序。
- ntohs(short类型的值)–网络到主机的short类型:将16位整数从网络字节顺序转换成主机的字节顺序。
对于IP地址来说,我们习惯使用点分十进制的方式来书写,下面的函数可以把点分十进制的字符串转换成一个可用的整数类型的IP地址:
inet_aton(char *ascii_addr,struct in_addr *network _addr)
反向的转换:
char* inet_ntoa(struct in_addr*network_addr)//返回指向字符串的指针
2.3 listen函数
函数原型:
int listen(int socket_fd,int backlog );
注意点:
- 返回值为0表示成功,-1表示失败
- 参数
- socket_fd表示socket函数创建的文件描述符
- backlog 表示支持的同时最大连接数,
此函数的意思是监听某个socket的连接,
2.4 accept函数
int accept(int sockfd, struct sockaddr* addr, socklen_t* len)
注意点:
- 返回值为0表示成功,否则表示失败,返回的值是一个新的套接字描述符
- accept函数的作用是等待来自客户端的连接,如果没有人连接则会一直阻塞
- 参数:
- sockfd 最开始的socket函数返回套接字描述符
- addr 是客户端的地址指针
- len 是指向客户端的地址的长度的指针
2.5 send函数
原型如下:
int send( int sockfd,const char *buf, int len, int flags );
首先解释一下这个函数,send函数是发送数据,参数解释如下:
- sockfd 最开始的socket函数返回套接字描述符
- buf 指向被发送数据的缓冲区的指针
- len 需要被发送数据的长度
- flags 一般为0,不知道具体含义
实际上每个socket,内部都有两个缓冲区,一个是发送缓冲区,一个是接收缓冲区。send函数的作用是把要发送的数据拷贝到发送缓冲区里。如果让发送缓冲区的长度设为:slen,发送缓冲区的剩余长度设为:slen_left那么执行流程如下:
send函数的返回值是拷贝到发送缓冲区的字节数。SOCKET_ERROR值是-1,如果拷贝数据出现错误或者是在等待协议程序发生缓冲区中的数据时4,网络断开,则返回-1,send函数把数据拷贝到缓冲区后就返回了,数据不一定会立刻被发送过去的。如果协议在之后的传送过程中出现错误的话,对面的socket会返回-1,
2.6 recv函数
函数原型:
int recv( int sockfd,const char *buf, int len, int flags );
参数以及返回值的含义与send函数很相似,recv函数作用是把接收缓冲区的数据拷贝到buf中,返回拷贝的字节数。在进行拷贝前,会先等待发送缓冲区中的数据发生完毕,然后看接收缓冲区是否有数据,如果没有收据或协议正在接收数据,就等待协议接收完毕。然后把数据从缓冲区拷贝到buf中,如果buf比较小需要多次拷贝。如果recv在等待接收数据时网络调用close(int sd)
函数,返回0,如果网络异常退出(客户端进程被杀死),返回-1。
2.7 connect函数
函数原型:
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
此函数用于客户端连接服务器的时候使用,参数:
- sockfd 套接字描述符
- serv_addr 地址结构
- addrlen 地址结构的长度 一般为
sizeof(sockaddr)
返回0表示成功,-1表示失败
3 代码示例
3.1 TCP代码
服务端代码
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#define PORT 10000//端口号
#define BACK_LOG 100//服务器允许同时在线的最大人数
#define BUFF_SIZE 1000//收发消息的缓冲区大小
char buf[BUFF_SIZE];//收发缓冲区
int main(int argc,char *argv[])
{
struct sockaddr_in server_add, client_add;//internet 地址结构
int ser_so_fd, cli_so_fd;//存放服务器,客户端的套接字描述符
int recv_nums;
int sockaddr_in_len=sizeof(server_add);
if ((ser_so_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) {//socket
printf("socket create failed\n");
return(ser_so_fd);
}
server_add.sin_family = AF_INET;
server_add.sin_port = htons(PORT);
server_add.sin_addr.s_addr = INADDR_ANY;
bzero(&(server_add.sin_zero), 8);//填充0
if (bind(ser_so_fd, (struct sockaddr *)&server_add, sizeof(server_add)) == -1) {//bind
printf("socket bind failed\n");
return(-1);
}
if (listen(ser_so_fd, BACK_LOG) == -1) {//listen
printf("socket listen failed\n");
return(-1);
}
while (1) {
cli_so_fd = accept(ser_so_fd, (struct sockaddr *)&client_add, &sockaddr_in_len);//接收一个用户
if ((recv_nums = recv(cli_so_fd, buf, BUFF_SIZE, 0)) > 0) {
buf[recv_nums]=0;
puts(buf);
send(cli_so_fd, "hello", 5, 0);
if(!strcmp("exit",buf))break;//收到exit服务器关闭
}
close(cli_so_fd);
}
close(ser_so_fd);
return 0;
}
客户端代码
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUFF_SIZE 1000//收发消息的缓冲区大小
char buf[BUFF_SIZE];
int main(int argc,char *argv[])
{
int sockfd,numbytes;
struct sockaddr_in addr;
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) {
printf("socket error\n");
}
addr.sin_family = AF_INET;
addr.sin_port = htons(10001);
addr.sin_addr.s_addr=inet_addr("127.0.0.1");
bzero(&(addr.sin_zero), 8);
if(connect(sockfd,(struct sockaddr*)&addr,sizeof(struct sockaddr)) == -1) {
printf("connect error !\n");
}
send(sockfd, "world", 5, 0);
numbytes = recv(sockfd, buf, BUFSIZ,0);//接收服务器端信息
buf[numbytes]='\0';
printf("%s",buf);
close(sockfd);
return 0;
}
3.2 UDP代码
服务端代码
//socket udp 服务端
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
int main()
{
//创建socket对象
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
//创建网络通信对象
struct sockaddr_in addr;
addr.sin_family =AF_INET;
addr.sin_port =htons(10001);
addr.sin_addr.s_addr=inet_addr("127.0.0.1");
//绑定socket对象与通信链接
int ret =bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
if(0>ret)
{
printf("bind\n");
return -1;
}
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
//UDP 不用listen
while(1)
{
char buf =0;
recvfrom(sockfd,&buf,sizeof(buf),0,(struct sockaddr*)&cli,&len);
printf("recv num =%hhd\n",buf);
buf =66;
sendto(sockfd,&buf,sizeof(buf),0,(struct sockaddr*)&cli,len);
}
close(sockfd);
}
客户端代码
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<arpa/inet.h>
int main()
{
//创建socket对象
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
//创建网络通信对象
struct sockaddr_in addr;
addr.sin_family =AF_INET;
addr.sin_port =htons(10001);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
while(1)
{
printf("请输入一个数字:");
char buf=66;
scanf("%hhd",&buf);
sendto(sockfd,&buf,
sizeof(buf),0,(struct sockaddr*)&addr,sizeof(addr));
socklen_t len=sizeof(addr);
recvfrom(sockfd,&buf,sizeof(buf),0,(struct sockaddr*)&addr,&len);
if(66 ==buf)
{
printf(" server 成功接受\n");
}
else
{
printf("server 数据丢失\n");
}
}
close(sockfd);
}