网络应用随处可见,表现的形式也各不相同,有趣的是,在不同的表现形式之下都是基于相同的编程模型,依赖于相同的编程接口,因此学习网络还是比较保值的,因为这么多基础设备还在运行着,基本机制也在短时间内很难改变。
网络编程的知识很多,包括进程,线程,信号等等,同时需要读者了解TCP/IP协议,本文假定读者已经了解熟悉这些相关知识,如果没有可能需要学习APUE,计算机网络这些基础知识了。本文的最终目标就是编写一个小型服务器。
客户端-服务器模型(C/S)是广泛使用的一种模型,一个应用是有一个服务器进程和多个客户端进程组成的,客户端发起请求,服务器做出响应,具体如下图:
认识到服务器和客户端是进程,而非主机是很重要的,因为网络编程是基于套接字的,所谓的网络通信也是一个主机的a进程和另一个主机的b进程进行通信,socket实际上也是进程通信,只不过是跨网络而已。
服务器和客户端通常运行在不同的主机上,中间还隔着异构的网络,十分复杂,好在前辈们设计出一套机制,TCP/IP协议栈帮我们封装这些细节,使我们感觉网络通信和主机内的通信很相似,我们忽略那些网络的细节,从编程的角度来理解如何进行网络编程。
对于一个主机而言,网络只是一个IO设备,用于数据接收和数据发送。具体来说,
从网络接收的数据从网络适配器经过IO总线到存储器,发送数据则相反,从主存到网络适配器。Linux下自然也符合这种特点,不过Linux具有更高层次的抽象,Linux的其中一个设计理念就是,一切皆文件。对于网络设备,Linux其实把它当作文件,因此接收发送数据其实也就是读写文件的操作,下面我们要讲socket编程就是基于此原理。
根据网络的应用范围和架构层级,可以分成三个部分:
1. 最底层 - Ethernet Segment
由若干主机(hosts)通过交换机(hub)连接,通常范围是房间或一层楼,如下图所示:
- 每个 Ethernet 适配器有一个唯一的 48 位的地址(也就是 MAC 地址),例如 00:16:ea:e3:54:e6
- 不同主机间发送的数据称为帧(frame)
- Hub会把每个端口发来的所有数据复制到其他的端口
- 所有的主机都可以看到所有的数据(注意安全问题)
2. 下一层 - Bridged Ethernet Segment
通常范围是一层楼,通过不同的 bridge 来连接不同的 ethernet segment。Bridge 知道从某端口出发可达的主机,并有选择的在端口间复制数据。
为了从概念上简化,我们可以认为,所有的 hub, bridge 可以抽象为一条线,如下图所示:
需要注意的是:之所以这么简化,是因为无论是集线器hub,还是网桥brige实际上都在一个局域网,网桥只是起着扩大的作用,下面我们所讲的路由器连接不同局域网。
3. 下一层 - internets
不同的(也许不兼容)的 LAN 可以通过 router (路由器)来进行物理上的连接,这样连接起来的网络称为 internet(注意是小写,大写的 Internet 可以认为是最著名的 internet)
通过这一层的封装,我们屏蔽不同异构网络的差别,使他们看起来好像就是在一个局域网中通信,为此,我们建立一套机制能够让某台源主机通过这些不兼容的网络发送数据到另一台主机。而这也就是著名的IP协议帮我们完成的,简单来说,它需要提供两种基本能力:
- 命名机制
不同的局域网技术有不同和不兼容的方式来分配主机地址,IP通过定义一种统一的主机地址格式消除差异,每个主机路由器都被分配至少一个IP地址,这个地址唯一表示这个主机。
- 传送机制
互联网协议通过将数据捆绑成为包的方式消除差异,一个包有包头和有效载荷组成,其中包头包括包的大小,源主机,目的主机地址等等,有效载荷就是要发送的数据。
IP地址
现在的网络编程依旧还在应用C/S模型,而这个模型又遵从TCP/IP协议,因此,我们重点学习TCP/IP相关的内容。
IP地址是一个32位无符号整数,具体来说,它定义在如下的一个结构体:
struct in_addr
{
unsigned int s_addr;
};
1. 因为主机可以有不同的字节序,TCP/IP定义了统一的网络字节序–大端字节序。而我们的主机字节序可是小端也可能是大端,因此Linux下提供下面的函数可以实现网络和主机字节序的转换:
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
2.
对于11_2题,先将字符串形式按照16进制读入,用htonl保证大端字节序,然后转化即可。
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("usage : [%s] [hex_number]\n", argv[0]);
exit(0);
}
// 0x8002c2f2
//
unsigned int addr;
sscanf(argv[1], "%x", &addr);
struct in_addr inaddr;
inaddr.s_addr = htonl(addr);//主机字节序->网络字节序 大段字节序
printf("%s\n", inet_ntoa(inaddr));
return 0;
}
对于11_3题,利用inet _aton函数将点分十进制的字符串转化为 in _addr类型,同时需要考虑本机字节序。
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("usage ; [%s] [ip]\n", argv[1]);
exit(0);
}
struct in_addr in;
inet_aton(argv[1], &in);
unsigned int addr;
addr = ntohl(in.s_addr);//转化为主机字节序
printf("0x%x\n", addr);
return 0;
}
域名解析
183.232.231.172这是一个IP地址,具体来说,他就是百度的一个IP地址,是不是很难记呢?为了便于人们记忆,我们利用Domain Naming System(DNS) 的概念,用来做 IP 地址到域名的映射,这样子我们访问网站只需要用域名即可,而不需要记忆那些数字了,我们可以利用nslookup命令查看~
套接字编程
客户端和服务器通过连接(connection)来发送字节流,特点是:
- 点对点: 连接一对进程
- 全双工: 数据同时可以在两个方向流动
- 可靠: 字节的发送的顺序和收到的一致
Socket 则可以认为是 connection 的 endpoint,socket 地址是一个 IPaddress:port 对。
Port(端口)是一个 16 位的整数,用来标识不同的进程,利用不同的端口来连接不同的服务:对于客户端,其端口是由内核随机分配的,成为临时端口;对于服务器,一般使用知名端口,比如Web服务器通常使用80;电子邮件使用25。。。
对于服务器,客户端我们只需要完成以上步骤就可以进行通信了。
我们通过编写一个echo服务器来熟悉以上socket函数的使用,具体功能如下:
client发送信息到server,server接收信息回显到client
效果图如下:
代码如下:
makefile
.PHONY:all
all:tcp_server tcp_client
tcp_server:tcp_server.c
gcc -o $@ $^ -static
tcp_client:tcp_client.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f tcp_client tcp_server
tcp_server
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
void usage(const char* arg)
{
printf("correct usage : %s [ip] [port]\n", arg);
}
int start_up(const char* ip, int port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1)
{
fprintf(stderr, "socket failure\n");
exit(-1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
inet_aton(ip, &local.sin_addr);
//local.sin_addr.s_addr = inet_addr(ip);
// int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
// 保证服务器能够立即终止和重启,默认重启是等待30秒
int opt = 1;
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
{
fprintf(stderr, "setsockopt failure\n");
exit(-1);
}
int flag = bind(sock, (struct sockaddr*)&local, sizeof(local));
if (flag == -1)
{
fprintf(stderr, "bind failure\n");
exit(-1);
}
flag = listen(sock, 6);
if (flag == -1)
{
fprintf(stderr, "listen failure\n");
exit(-1);
}
return sock;
}
int main(int argc, char* argv[])
{
// ./tcp_server ip port
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int listen_sock = start_up(argv[1], atoi(argv[2]));
char buf[1024];
while (1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
// int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int new_fd = accept(listen_sock, (struct sockaddr*)&client, &len);
if (new_fd == -1)
{
fprintf(stderr, "accept failure\n");
continue;
}
printf("a new client connects the server\n");
while (1)
{
memset(buf, 0, sizeof(buf));
ssize_t s = read(new_fd, buf, sizeof(buf) - 1);
if (s > 0)
{
buf[s] = 0;
printf("client [%s:%d] # %s\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port), buf);
write(new_fd, buf, strlen(buf));
}
else if (s == 0)
{
printf("client quits\n");
close(new_fd);
break;
}
else
{
printf("read failure\n");
close(new_fd);
break;
}
}
}
return 0;
}
tcp_client
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
void usage(const char* arg)
{
printf("correct usage: %s [remote_ip] [remote_port]\n", arg);
}
int open_clientfd(const char* ip, int port)
{
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
fprintf(stderr, "socket failure\n");
exit(-1);
}
struct sockaddr_in remote;
remote.sin_family = AF_INET;
remote.sin_port = htons(port);
inet_aton(ip, &remote.sin_addr);
int flag = connect(clientfd, (struct sockaddr*)&remote, sizeof(remote));
if (flag == -1)
{
fprintf(stderr, "connect failure\n");
exit(-1);
}
return clientfd;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int sock_fd = open_clientfd(argv[1], atoi(argv[2]));
while (1)
{
char buf[1024];
printf("please enter # ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if (s > 0)
{
buf[s - 1] = '\0';
write(sock_fd, buf, strlen(buf));
//server echo
s = read(sock_fd, buf, strlen(buf));
if (s > 0)
{
printf("server echo # %s\n", buf);
}
}
}
return 0;
}
tcp_server
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
void usage(const char* arg)
{
printf("correct usage: %s [remote_ip] [remote_port]\n", arg);
}
int open_clientfd(const char* ip, int port)
{
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (clientfd == -1)
{
fprintf(stderr, "socket failure\n");
exit(-1);
}
struct sockaddr_in remote;
remote.sin_family = AF_INET;
remote.sin_port = htons(port);
inet_aton(ip, &remote.sin_addr);
int flag = connect(clientfd, (struct sockaddr*)&remote, sizeof(remote));
if (flag == -1)
{
fprintf(stderr, "connect failure\n");
exit(-1);
}
return clientfd;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int sock_fd = open_clientfd(argv[1], atoi(argv[2]));
while (1)
{
char buf[1024];
printf("please enter # ");
fflush(stdout);
ssize_t s = read(0, buf, sizeof(buf)-1);
if (s > 0)
{
buf[s - 1] = '\0';
write(sock_fd, buf, strlen(buf));
//server echo
s = read(sock_fd, buf, strlen(buf));
if (s > 0)
{
printf("server echo # %s\n", buf);
}
}
}
return 0;
}