这几天在看muduo网络库,顺便第二次详细的精读一下《unix网络编程》。
在这里从最基础的编程模型开始,记录一下一步步改进程序的过程和细碎的知识点。
首先看一下启动一个服务器程序所必须的库函数。
#include <sys/socket.h>
int socket(int family,int type,int protocol);
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *servaddr, socklen_t addrlen);
返回:若成功返回0,出错返回-1
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
#include <sys/socket.h>
int listen(int sockfd,int backlog);
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr, socklen_t *addrlen);
返回:成功返回非负描述符,出错返回-1
这里给出一个返回服务器时间的小例子,我的代码是运行在cloud9平台上的,非常好的一个在线集成环境,我选的是C++编程环境。它会自动生成一个makefile文件,简单定义makefile就是帮助你完成编译的脚本文件,你也可以选择在命令行一个一个输入编译命令。文件大致如下:
all: hello-cpp-world hello-c-world
%: %.cc g++ -std=c++11 $< -o $@ %: %.c gcc $< -o $@
下面是服务器代码,大致流程跟开始的图片流程一样:
#include <iostream>
#include "unp.h"
void errorHandling(const char *message); // 错误处理
int main() {
int listnenfd,connfd;
struct sockaddr_in servaddr;
//用来存储时间字符串的缓冲区
char buff[MAXLINE];
//测试客户端返回数据用的缓冲区
char readBuf[MAXLINE];
time_t ticks;
//建立套接字
listnenfd = socket(PF_INET,SOCK_STREAM,0);
//代替memset进行初始化的函数
bzero(&servaddr, sizeof(servaddr));
//设置协议族
//IPV4
servaddr.sin_family = AF_INET;
//这里出现一个htonl函数,是将host主机字节序转换成network网络字节序
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//监听的端口
servaddr.sin_port = htons(8081);
//绑定端口
if(-1== bind(listnenfd,(SA*)&servaddr, sizeof(servaddr)))
errorHandling("socket_error");
//开始监听
if(-1==listen(listnenfd,LISTENQ))
errorHandling("listen() error");
//std::cout<<"now listen"<<std::endl;
//现在服务器属于迭代服务器,一次只能服务一个连接,这当然是不太有效率的
for(;;){
connfd = accept(listnenfd, (SA*)NULL, NULL);
//read(connfd,readBuf,sizeof(readBuf)-1);
//printf("%s",readBuf);
//获取时间写入缓冲区后发送出去
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
//以后可以用sendmsg等代替
write(connfd, buff, strlen(buff));
close(connfd);
}
std::cout<<"test "<<std::endl;
}
void errorHandling(const char *message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
用到的头文件
#ifndef _unp_h
#define _unp_h
#include <sys/time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <error.h>
#define SA struct sockaddr
#define MAXLINE 1024
#define LISTENQ 1024
#define CPU_VENDOR_OS "Linux"
#endif
编译后在后台运行
g++ server.cc -o server
./server &
客户端程序:
#include "unp.h"
#include "myerr.h"
int main(int argc,char** argv){
int sockfd,n;
char recvline[MAXLINE+1];
struct sockaddr_in servaddr;
if(argc != 2)
err_quit("usage: a.out<IPaddress>");
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
err_sys("socket error");
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(8081);
//这里客户端带参数,下面的函数将字符串装换成二进制结果存放在sin_addr中
if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
err_quit("inet_pton error for %s",argv[1]);
if(connect(sockfd,(SA *)&servaddr, sizeof(servaddr)) <0 )
err_sys("connect error");
//读取到服务器发送的字节后输出到标准输出
while((n = read(sockfd, recvline, MAXLINE)) >0 ){
recvline[n] = 0;
if(fputs(recvline, stdout) == EOF)
err_sys("fputs error");
}
if (n < 0)
err_sys("read error");
exit(0);
}
编译并运行程序
g++ daytimetcpcli.cc -o client
./client 127.0.0.1
结果:
Tue Apr 5 13:54:12 2016
这里顺便介绍几个常用指令:
//显示所有网络端口信息
netstat -na
//提供网络接口信息
netstat -ni
//有了接口名字,获得接口的详细信息
ifconfig eth0
//找出本地网络众多主机,通过上面出现的广播地址ping
ping -b XXX.XXX.XXX.XXX
上面的服务器是迭代的,如何实现一个并发的服务器,能给多个连接同时提供服务呢?最简单的模型是阻塞式的fork+exec。
#include <unistd.h>
pid_t fork(void);
返回:在子进程中为0,在父进程中为子进程ID
正因为如此,父子进程共享很多东西,例如所有父进程打开的描述符。通常,父进程调用accept后fork,然后子进程接着读写这个套接字,父进程关闭此套接字。
下面是一个并发服务器的简单轮廓:
pid_t pid;
int listenfd,connfd;
listenfd = socket(...);
bind(listenfd,...);
listen(listenfd,LISTENQ);
//为什么不用while(1)大概是为了省略一次判定,但是编译器可能已经优化过了
for(;;){
connfd = accept(listenfd,...);
if(( pid = fork()) == 0){
close(listenfd);
doit(connfd);
close(connfd);
exit(0);
}
close(connfd);
}
这里有一个小细节,为什么在父进程中的close不会直接终止和客户的连接呢?因为每个文件和套接字都有一个引用计数,概念和智能指针中的引用计数类似,计数值为0才真正关闭描述符,当然想发个TCP的FIN分节也是有办法的,shutdown函数。
期待下一次的改进。