百行代码编写httpserver及基本函数介绍

 本文介绍一个最简单的Linux下服务器项目及程序编写,整个工程仅一百余行代码,非常适合想从事Linux及后台开发相关工作的新手入门(文末附源码)。

一、结果展示

Linux环境下打开终端并进入工程所在文件夹,先编译工程再运行程序:

其中8888为4位端口号。执行程序后打开Linux环境自带的火狐浏览器(FireFox),输入

http://localhost:8888/index.html

并回车,之后浏览器就会显示出工程文件夹内HTML文件中的内容。 

二、项目简介

网络编程,或者说服务器编程,就是使网络上的两个应用程序之间相互通信的过程。这相当于你和朋友在各自家中使用固定电话聊天的过程。整个过程可分为以下几步

1.相关函数介绍

(1)创建套接字(分配电话号码)

第一步是给你家分配电话号码,称为创建套接字,使用下面这个函数进行。

#include <sys/socket.h>
int socket(int af, int type, int protocol);

 其中,af为地址/协议族,也就是 IP 地址类型,常用的有 AF_INET (PF_INET)和 AF_INET6(PF_INET6)。AF 是“Address Family”的简写,PF 是“Protocol Family”的简写,INET是“Inetnet”的简写。AF_INET 表示 IPv4 地址,AF_INET6 表示 IPv6 地址。

 type 为数据传输方式/套接字类型,常用的有 SOCK_STREAM(流格式套接字/面向连接的套接字) 和 SOCK_DGRAM(数据报套接字/无连接的套接字)。

大部分情况下,传递前两个参数即可创建所需套接字,因此可将第三个参数置0。除非同一协议族中存在多个数据传输方式相同的协议。

(2)绑定套接字与本地地址(安装固定电话)

分配好电话号码后就要将这个号码与你家地址进行绑定,使用下面这个函数

#include<sys/socket.h>

int bind(int sockfd,  struct sockaddr *myaddr, socklen_t addrlen);

第一个参数即为上一步创建的套接字,第二个参数是指向特定协议的地址结构的指针(下文介绍),第三个参数为该地址结构的长度。

(3)等待接听电话

#include<sys/socket.h>

int listen(int sockfd, int backlog);

把套接字转化成可接受连接的状态。第一个参数定义如前所述,第二个参数为系统最大连接数,即允许同时给你打电话的最大人数,多于此的连接请求将被拒绝。

(4)接听电话

#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

接受对方的连接请求。具体参数定义如前所述。

2.线程简介

服务器使用多线程来同时处理多条连接,每个线程共享数据区和堆区,同时有自己的栈区。使用以下这个函数创建线程。

#include <pthread.h>
int pthread_create(
    pthread_t *restrict thread, const pthread_attr_t *restrict attr,
    void *(* start_routine)(void *), void *restrict arg
);

第一个参数为线程ID的变量地址;第二个用于传递线程属性,默认为NULL;第三个是线程执行的函数的函数指针;第四个为线程执行函数的参数。

#include <pthread.h>
int pthread_detach(pthread_t tid);

主线程与子线程分离,子线程结束后,资源自动回收。

3.相关结构体

struct sockaddr_in
{
    sa_family_t    sin_family;    //地址族(IPV4或IPV6)
    uint16_t       sin_port;      //16位TCP/UDP端口号
    struct in_addr sin_addr       //32位IP地址
    char           sin_zero[8]    //不使用
};

struct in_addr
{
    in_addr_t      s_addr;
};

第一个结构体作为地址信息传递给bind函数,第二个结构体用来存放32位IP地址。其中in_addr_t为uint32_t类型。

 4.字节序与网络地址转换

unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

上面四个函数为字节序转换函数。其中,h代表主机(host),n代表网络(net)。

#include <arpa/inet.h>

in_addr_t inet_addr(const char* string);
int inet_aton(const char* string, struct in_addr* addr);
char* inet_ntoa(struct in_addr adr);

以上三个函数的前两个将字符串形式IP地址转换为网络字节序整数并返回,第三个将网络字节序整数型IP地址转换为字符串形式。

三、其他函数介绍

1.IO函数

readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。

#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);
两个函数的返回值:若成功则返回已读、写的字节数,若出错则返回-1

这两个函数的第二个参数是指向iovec结构数组的一个指针:

struct iovec {
    void      *iov_base;      /* starting address of buffer */
    size_t    iov_len;        /* size of buffer */
};

writev以顺序iov[0],iov[1]至iov[iovcnt-1]从缓冲区中聚集输出数据。writev返回输出的字节总数,通常,它应等于所有缓冲区长度之和。

readv则将读入的数据按上述同样顺序散布到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。readv返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。

int send(SOCKET s, const char FAR *buf, int len, int flags );  
用send函数来向TCP连接的另一端发送数据。
该函数的第一个参数指定发送端套接字描述符;
第二个参数指明一个存放应用程序要发送数据的缓冲区;
第三个参数指明实际要发送的数据的字节数;
第四个参数一般置0。

int recv( SOCKET s, char FAR *buf, int len, int flags );  
用recv函数从TCP连接的另一端接收数据。
该函数的第一个参数指定接收端套接字描述符;
第二个参数指明一个缓冲区,该缓冲区用来存放recv函数接收到的数据;
第三个参数指明buf的长度;
第四个参数一般置0。

read和write
1、write()

函数定义:ssize_t write(int fd, const void * buf, size_t count); 

函数说明:write()把参数buf所指的内存写入count个字节到参数fd所指的文件内。

返回值:如果顺利write()会返回实际写入的字节数(len)。当有错误发生时则返回-1,错误代码存入errno中。

2、read()

函数定义:ssize_t read(int fd, void * buf, size_t count);

函数说明:read()把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。

返回值:返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据。若参数count 为0, 则read()不会有作用并返回0。

2.内存映射函数 mmap()

mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上。必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。

#include <sys/mman.h>
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);

start:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
length:代表将文件中多大的部分映射到内存。

prot:映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取

flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。

fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。
offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。

若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。

3.vsnprintf()函数

#include <stdarg.h>
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
将可变参数格式化输出到一个字符数组
参数:str输出到的数组,size指定大小,防止越界,format格式化参数,ap可变参数列表函数用法

例:

bool http_conn::add_response(const char* format, ...)
{
    va_list arg_list;
    va_start(arg_list, format);

    int len = vsnprintf(m_write_buf, len, format, arg_list);
    va_end(arg_list);
    return true;
}

VA_LIST 是在C语言中解决变参问题的一组宏。用法示例:

#include <stdarg.h> 
int AveInt(int,...);
void main()
{
   printf("%d/t",AveInt(2,2,3));
   printf("%d/t",AveInt(4,2,4,6,8));
 
   return;
}
 
int AveInt(int v,...)
{
   int ReturnValue=0;
   int i=v;
 
   va_list ap;//首先在函数里定义va_list变量,这个变量是指向参数的指针
   va_start(ap,v);//用VA_START宏初始化刚定义的VA_LIST变量;
 
   while(i>0)
   {
//用VA_ARG返回可变的参数,VA_ARG的第二个参数是返回参数的类型
//(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数)
       ReturnValue+=va_arg(ap,int);
       i--;
   }
   va_end(ap); //最后用VA_END宏结束可变参数的获取
   return ReturnValue;
}

4.stat结构体

struct stat这个结构体是用来描述一个linux系统文件系统中的文件属性的结构。

int stat(const char *path, struct stat *struct_stat);
int lstat(const char *path,struct stat *struct_stat);
两个函数的第一个参数都是文件的路径,第二个参数是struct stat的指针。返回值为0表示成功执行。
执行失败时设置error

两个函数区别在于stat没有处理字符链接(软链接)的能力,如果一个文件是符号链接,stat会直接返回它所指向的文件的属性;而lstat返回的就是这个符号链接的内容。目录在linux中也是一个文件,文件的内容就是这个目录下面所有文件与inode的对应关系。硬链接就是在某一个目录下面将一个文件名与一个inode关联起来,其实就是添加一条记录;而软链接也叫符号链接,这个文件的内容就是一个字符串,这个字符串就是它所链接的文件的绝对或者相对地址。

struct stat {
        mode_t     st_mode;       //文件对应的模式,文件,目录等
        ino_t      st_ino;       //inode节点号
        dev_t      st_dev;        //设备号码
        dev_t      st_rdev;       //特殊设备号码
        nlink_t    st_nlink;      //文件的连接数
        uid_t      st_uid;        //文件所有者
        gid_t      st_gid;        //文件所有者对应的组
        off_t      st_size;       //普通文件,对应的文件字节数
        time_t     st_atime;      //文件最后被访问的时间
        time_t     st_mtime;      //文件内容最后被修改的时间
        time_t     st_ctime;      //文件状态改变时间
        blksize_t st_blksize;    //文件内容对应的块大小
        blkcnt_t   st_blocks;     //文件内容对应的块数量
};
stat结构体中的st_mode定义了下列数种情况:
    S_IFMT   0170000    文件类型的位遮罩
    S_IFSOCK 0140000    scoket
    S_IFLNK 0120000     符号连接
    S_IFREG 0100000     一般文件
    S_IFBLK 0060000     区块装置
    S_IFDIR 0040000     目录
    S_IFCHR 0020000     字符装置
    S_IFIFO 0010000     先进先出

    S_ISUID 04000     文件的(set user-id on execution)位
    S_ISGID 02000     文件的(set group-id on execution)位
    S_ISVTX 01000     文件的sticky位

    S_IRUSR(S_IREAD) 00400     文件所有者具可读取权限
    S_IWUSR(S_IWRITE)00200     文件所有者具可写入权限
    S_IXUSR(S_IEXEC) 00100     文件所有者具可执行权限

    S_IRGRP 00040             用户组具可读取权限
    S_IWGRP 00020             用户组具可写入权限
    S_IXGRP 00010             用户组具可执行权限

    S_IROTH 00004             其他用户具可读取权限
    S_IWOTH 00002             其他用户具可写入权限
    S_IXOTH 00001             其他用户具可执行权限

三、源码地址

源码地址nanjingu/WebServer_version0 (github.com)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值