本文介绍一个最简单的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 其他用户具可执行权限