前言
本文所搭建的是局域网内可访问的web服务器,主要功能包括ls目录和cat文件。使用HTTP协议,通过浏览器与服务器进行通信。为防止遗忘,特此写下这篇博客,有不妥之处还望指正。
主要操作
服务器/客户端的模型类似于我们日常打电话。服务器搭建与工作流程可以分为六个步骤,每个步骤对应于一个系统调用(参考《UNIX/LINUX编程实践教程》)。
行为 | 系统调用 |
---|---|
1获取电话线 | socket |
2.分配号码 | bind |
3.允许接入调用 | listen |
4.等待电话 | accept |
5.传送数据 | read/write |
6.挂断电话 | close |
函数介绍
socket: 在man手册里介绍,它创建一个通信端点。返回一个socket描述字,它存在于协议族(address family,AF_XXX)空间中,但没有一个具体地址。如果想给他复制一个地址,必须调用bind()函数。它类似于获取一根电话线。它的函数原型为:
#include<sys/types.h>
#include<sys/socket.h>
int socket(int domain,int type,int protocal);
参数domain定义了该socket的通信域,我们常用的是AF_INET(ipv4)和AF_INET6(ipv6),还用AF_UNIX,它用于本地通信。
参数type:指出了程序将要使用的数据类型。在本文我们使用SOCK_STREAM,它是双向的管道类型,数据作为连接的字节流从一段写入,再从另一端流出。
参数protocol:内核中网络代码所使用的协议,并不是客户端和服务器之间的协议。一个0的值代表选择标准的协议。
bind:把一个地址分配给socket。该地址分配类似于把电话号码分配给一根电话线。它的函数原型为:
#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen)
参数sockfd:socket系统调用返回的socket描述字。
参数struct sockaddr *addr:包含了需要绑定的socket的基本信息,比如地址、通信域、端口号等。但一般编程中并不直接针对次数据结构操作,而是使用另一个与sockaddr等价的数据结构,因为本文搭建的是web服务器,且通信域为ipv4,所以使用与之等价的sockaddr_in结构,其定义在/usr/include/netinet/in.h中。
listen:坚挺socket上的连接,函数原型为:
#include<sys/types.h>
#include<sys/socket.h>
int listen(int sockfd,int backlog)
参数sockfd:socket系统调用返回的socket描述字。
参数backlog:sockfd的等待队列的最大长度。
accept:接收socket上的一个连接,就像接电话拿起话筒,它会返回一个文件描述符,此时的网络I/O操作就类似于文件I/O操作。accept系统调用是否阻塞取决于socket描述字的属性,可以使用fcntl函数来对socket描述字进行设置O_NOBLOCK,函数原型为:
#include<sys/types.h>
#include<sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,sockklen_t *addrlen)
参数sockfd:socket系统调用返回的socket描述字。
参数*addr:储存连接到服务器连接的信息,类似于来电显示,其中包含了连接地址、连接端口号等信息。
read()/write():在前一步骤中,accept()返回了一个文件描述符,此时就可以想文件IO一样对其进行操作,可以使用系统调用read()和write(),也可以使用fdopen()函数,将其转换成文件流的方式进行操作。
close(fd):类似于挂断电话,关闭这个文件描述符相当于结束此次连接。
搭建服务器
搭建服务器的步骤上述已经描述,本节姜葱代码层面进行分析讲解。搭建服务器的代码独立封装成文件socklib.c,以便于后续调用。文件中也封装了connect_to_server的函数,与搭建服务器大同小异,我就偷个懒了。
//函数:make_server_socket
//参数:portname:端口号;
// queue:队列长度;
//返回值:-1:失败;
// sockid:成功并返回socket描述字;
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<netdb.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<strings.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<ifaddrs.h>
#include<linux/if_link.h>
#include<string.h>
#define oops(m) {perror(m);exit(1);}
int make_server_socket(int portname,int queue)
{
int sock_id;
struct sockaddr_in saddr;
struct hostent *hp;
char hostname[100];
// char **pptr;
// char str[32];
// struct ifaddrs *ifaddr,*ifa;
// int family,n;
// char host[NI_MAXHOST];
if((sock_id=socket(PF_INET,SOCK_STREAM,0))==-1)//获取socket描述字
{
oops("socket");
return -1;
}
if((gethostname(hostname,100))==-1)//获取本主机的标准主机名
{
oops("gethostname");
return -1;
}
bzero((void *)&saddr,sizeof(saddr));//初始化saddr;
hp=gethostbyname(hostname);//返回包含本机主机名字和地址信息的hostent结构体指针。用域名或主机名获取IP地址。
bcopy((void *)hp->h_addr,(void *)&saddr.sin_addr,hp->h_length);
//inet_aton(host,(struct in_addr *)&saddr.sin_addr.s_addr);
saddr.sin_port=htons(portname);//将整形变量从主机字节书序变成网络字节顺序。
saddr.sin_family=AF_INET;
if((bind(sock_id,(struct sockaddr *)&saddr,sizeof(saddr)))==-1)//绑定sockid
{
oops("bind");
return -1;
}
if((listen(sock_id,queue))==-1)//监听sockid,最大连接数queue
{
oops("listen");
return -1;
}
return sock_id;
}
//函数connect_to_server:
//参数:*host:主机名:
// postname:端口号;
int connect_to_server(char *host,int postname)
{
int sock_id;
struct sockaddr_in saddr;
struct hostent *hp;
if((sock_id=socket(AF_INET,SOCK_STREAM,0))==-1)
{
oops("socket");
return -1;
}
bzero(&saddr,sizeof(saddr));
hp=gethostbyname(host);
if(hp==NULL)
{
//oops("gethostbyname");
printf("gethostbyname error\n");
return -1;
}
bcopy(hp->h_addr,(struct sockaddr *)&saddr.sin_addr,hp->h_length);
//printf("%d\n",hp->h_length);
saddr.sin_port=htons(postname);
saddr.sin_family=AF_INET;
if((connect(sock_id,(struct sockaddr *)&saddr,sizeof(saddr)))!=0)
{
oops("connect");
return -1;
}
printf("connect success\n");
return sock_id;
}
简单介绍下在make_server_socket中涉及到几个函数:gethostname,gethodtbyname,htons.
int gethostname(char name,size_tlen);
这个函数获取当前主机的主机名;
struct hostent *gethostname(const char *name);
这个函数返回对应于主机名的hostent结构指针。name可以是主机名也可以是标准点表示法的IPV4地址。我们这里使用的是主机名。它返回一个hostent结构体。 hoetent结构体:
struct hostent{
char *h_name; //表示主机的规范名,如www.google.com;
char **h_aliases; //表示主机别名,如google;
int h_addrtype; //表示主机ip地址的类型。即ipv4或者ipv6;
int h_length; //表示主机ip地址的长度;
char **h_addr_list; //表示主机的ip地址。这个是网络字节顺序存储,不能用printf的%s格式打印,需要的话调用inet_ntop();
#define h_addr h_addr_list[0]
}
实现时间查询服务器
在上一小节简述了如何搭建服务器,并封装了make_server_socket和connect_to_server两个函数,在这一节讲简述如何利用前面编写的socklib运作和访问服务器。本文以带时间查询功能的服务器为例。
直接上代码:
dateser.c
#include "socklib.h"
#include<stdio.h>
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<netinet/in.h>
#include<stdlib.h>
#define oops(m) {perror(m);exit(1);}
int process_request(int fd)
{
int pid=fork();
switch(pid)
{
case -1: return -1;
case 0: dup2(fd,1);
close(fd);
execlp("date","date",NULL);
oops("execlp");
default:wait(NULL);
}
}
int main()
{
int sock_server,sock_client;
struct sockaddr_in saddr;
int len;
// printf("begin\n");
if((sock_server=make_server_socket(13000,1))==-1)
{
printf("sock_server error\n");
exit(1);
}
while(1)
{
printf("wait for connect\n");
if((sock_client=accept(sock_server,(struct sockaddr *)&saddr,&len))==-1)
break;
printf("connected\n");
//printf("get a connect:%d %d\n",saddr.sin_addr.s_addr,saddr.sin_port);
process_request(sock_client);
printf("over\n");
close(sock_client);
}
}
dateclient.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include"socklib.h"
void talk_with_server(int sock_fd)
{
char buf[BUFSIZ];
int n;
n=read(sock_fd,buf,BUFSIZ);
write(1,buf,n);
}
void del_str_line(char *str)
{
char *p=str;
while(*p!='\n')
{
p++;
if(*p=='\0')
break;
}
*p='\0';
}
int main()
{
int sock_fd;
char hostname[100],portname[50];
int port;
printf("hostname:");
fflush(stdout);
read(0,hostname,100);
del_str_line(hostname);
printf("port:");
fflush(stdout);
read(0,portname,50);
port=atoi(portname);
printf("hostname:%s port:%d\n",hostname,port);
// fflush(stdout);
if((sock_fd=connect_to_server(hostname,port))==-1)
{
printf("sock_fd error%d\n",sock_fd);
exit(1);
}
talk_with_server(sock_fd);
close(sock_fd);
return 1;
}
完成以上代码后进行编译执行。注意要写一下socklib.h头文件,这里就不贴了。还要注意的是在dataserver中的端口号我直接定的13000,你也可以通过命令含参数进行自定义。
yang@yang-Inspiron-5437:~/桌面/study/csdn$ sudo gcc socklib.c dateser.c -o dateser
yang@yang-Inspiron-5437:~/桌面/study/csdn$ sudo gcc socklib.c dateclient.c -o dateclient
yang@yang-Inspiron-5437:~/桌面/study/csdn$ ./dateser &
[1] 17019
yang@yang-Inspiron-5437:~/桌面/study/csdn$ wait for connect
yang@yang-Inspiron-5437:~/桌面/study/csdn$ telnet 127.0.1.1 13000
Trying 127.0.1.1…
Connected to 127.0.1.1.
Escape character is ‘^]’.
connected
2020年 05月 21日 星期四 17:59:50 CST
over
wait for connect
Connection closed by foreign host.
yang@yang-Inspiron-5437:~/桌面/study/csdn$ ./dateclient
hostname:127.0.1.1
port:13000
hostname:127.0.1.1 port:13000
connect success
connected
2020年 05月 21日 星期四 18:00:05 CST
over
wait for connect
yang@yang-Inspiron-5437:~/桌面/study/csdn$
asdas
注意哦,我使用的hostname是直接用的ipv4地址,也可以是对应的主机名,可以在cat /etc/hosts查看。
最后
本文讲解的是给予本机的服务器搭建,即只能本机访问,别的电脑无法访问,原因是bind绑定的是主机的ip(127.0.0.1),而不是局域网内的ip(192.168.x.x),如果绑定是局域网,那么同局域网的其他电脑即可访问。这一部分将在ubuntu下搭建简易web服务器(二)中进行简述。
声明:作者主要通过《UNIX/LINUX编程实践教程》的学习完成本文。