说在前面
- 环境: ubuntu16.04
- 参考: UNIX网络编程
概述
- 编写网络通信程序首先要明确使用的通信协议。同时,需要在高层次(例如应用层?)确定发起通信的程序以及响应的时间。(如Web客户端与Web服务器之间的通信,服务器需要长时间保持运行,以响应随时可能出现的客户端请求。)
通常使用客户/服务端模型(C/S模型)。
一个客户端可能与多个服务器通信;一个服务器也可能同时与多个客户端通信。 - 客户端程序与服务端之间的通信通常涉及多个网络层协议。
实例-获取时间
-
实验环境说明
由于该实验需要运行客户端以及服务端程序,所以可能会有以下实验环境(假定操作系统均为Linux,不同环境的处理见后面的内容):
- 局域网下两台机器分别运行
随意选择运行客户端还是服务端。 - 宿主机+虚拟机(如VMware)
随意选择运行客户端还是服务端。Windows环境可以[运行两台虚拟机]或者[宿主机使用WSL+一台虚拟机] - 一台主机+云服务器
云服务器(拥有公网IP)运行服务端程序,主机运行客户端程序。 - 同一台主机上运行客户端以及服务端
不讨论这种情况
- 局域网下两台机器分别运行
-
客户端
-
一般流程
-
创建套接字
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) err_sys("socket error");
通过socket函数创建网际(AF_INET)字节流(SOCK_STREAM)套接字。函数返回一个整数,用于标识该套接字。
补充说明: 在linux下,我们可以将所有的设备都看作文件来处理,并使用一个文件描述符(FileDescriptor,一个整型数)来标识它们。
err_sys函数非标准函数,用于出错处理,主要功能为打印错误信息并终止程序。具体实现见代码。
-
建立连接
struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(13); if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) err_quit("inet_pton error for %s", argv[1]); if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0) err_sys("connect error");
-
连接服务端需要知道目的IP以及端口号,我们使用结构体sockaddr_in来存储。
-
定义servaddr变量后使用bzero函数(该函数可以使用memset函数代替)清零。
-
然后将地址族sin_family置为AF_INET,网络序的端口号13填入到sin_port。
htonl函数,主机序转换为网络序;ntohl函数,网络序转换为主机序。
-
inet_pton函数可以将点分十进制串(如192.168.12.1)转换为适合sin_addr的格式,并且如果转换出错(即字符串不符合点分十进制格式)会返回负值。
-
准备完成后使用connect函数与servaddr指向的服务器建立一个TCP连接。若建立连接失败,返回负值。
-
-
读/写
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) { recvline[n] = 0; if (fputs(recvline, stdout) == EOF) err_sys("fputs error"); }
使用read函数读取服务器返回的数据。函数返回值为实际读取的字节数;若未读取到数据,返回负值。
从使用read函数也可以看出,linux将通信设备看作"文件"来处理,服务器返回的数据会先存放在该"文件"中,通过read系统调用来读取这些数据。在发送数据时,我们通过wirte系统调用往某类似"文件"中写入数据,再由操作系统将"文件"中的数据发送出去。
-
关闭套接字
if(close(sockfd) == -1) err_sys("close error");
-
不同实验环境
主要的差异在于IP地址。
前两种环境下,绑定的ip即主机或虚拟机的ip,可以使用ifconfig命令ifconfig
第三种环境下,绑定的为服务器的公网ip,同时注意防火墙是否打开了对应的端口(或者直接关闭防火墙)
-
-
服务端
-
一般流程
-
创建套接字
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) err_sys("socket error");
-
绑定
bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(13); if(bind(listenfd, (struct sockaddr_in *) &servaddr, sizeof(servaddr)) < 0) err_sys("bind error");
同样,现将结构体变量清零,同时将地址族sin_family置为AF_INET,ip地址置sin_addr.s_addr为INADDR_ANY(即0x00000000;一个主机可能有多个网络接口,例如多块网卡,这样我们可以接受任意网络接口上的连接),sin_port为13。
sin_addr实际为一个(in_addr类型)结构体变量,只有一个成员s_addr。
struct in_addr {
in_addr_t s_addr;//32位
};使用bind函数将13号端口捆绑至已创建的套接字上。
-
监听
if(listen(listenfd, LISTENQ) < 0) err_sys("listen error");
使用listen函数将套接字转换为监听套接字。
-
接受连接
for ( ; ; ) { if( (connfd = accept(listenfd, (struct sockaddr_in *) NULL, NULL)) < 0) { if(errno == EPROTO || errno == ECONNABORTED) continue; else err_sys("accept error"); } // 、、、 }
通常,进程在调用accept函数进入睡眠,等待客户端连接。函数返回值是一个称为已连接描述符(connected descriptor)的新描述符。accept为每个连接的客户端分配一个新描述符。通过该描述符,可以与客户端进行通信。
补充: 在accept函数被调用时,可能会出现非致命错误(ECONNABORTED/EPROTO),这时只需再次调用accept函数即可解决。
-
读/写
ticks = time(NULL); snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks)); if( write(connfd, buff, strlen(buff)) != strlen(buff)) err_sys("write error");
获取服务器时间,并用write函数将该时间发送给客户端。
-
关闭套接字
if (close(connfd) == -1) err_sys("close error");
-
不同实验环境
三种不同实验环境下没有啥区别,注意防火墙以及对应的端口即可。
-
-
UNIX网络编程一书中的基本流程
贴图
- 作为server的WSL,关闭防火墙。
- 作为client的另一台主机
代码
- github
- 编译
cd getdaytime make