网络编程总结(一)

  这几天在看muduo网络库,顺便第二次详细的精读一下《unix网络编程》。
  在这里从最基础的编程模型开始,记录一下一步步改进程序的过程和细碎的知识点。
  首先看一下启动一个服务器程序所必须的库函数。
  
网络编程总结(一)_第1张图片

  • socket
#include <sys/socket.h>
int socket(int family,int type,int protocol);
  1. family参数指明协议族,AF_INET(ipv4),AF_INET(ipv6)等。
  2. type参数指明套接字类型,TCP是字节流协议,仅支持sock_stream。
  3. protocol参数指明传输协议,共有TCP,UDP,SCTP三类。
  4. AF_XXX与PF_XXX通常没有区别。
  • connect
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr *servaddr, socklen_t addrlen);
                        返回:若成功返回0,出错返回-1
  1. sockfd是socket函数返回的套接字描述符,二参数为指向套接字地址结构的指针,三为该结构大小。
  2. 在connect函数中将激发TCP三次握手。为什么TCP需要三次握手?而不是两次或者更多,首先因为理论上,三次是确保可靠的基本次数,而不是保证能可靠的次数。其次它能防止无效的报文请求连接,比方一段请求的报文因为网络延迟一直被滞留,等到连接释放后才到达server,这时候server会发确认,client并不理睬,server会一直阻塞等待client的数据,server的资源就被白白浪费了。
  3. 函数的出错返回有几种情况,大致上分为timeout,这时候一般会有重传机制;服务器响应RST,指定的端口上没有进程等待连接;路由器上的destination unreachable。
  • bind
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
  1. 主要有两种用法,一是服务器启动的时候绑定一个发布给大家的端口。
  2. 二是把特定IP地址绑定到它的套接字上。
  3. bind可以指定IP和端口号,也可以不指定。对于IPV4,常量值INADDR_ANY是通配地址,一般为0,由内核选择IP。
  • listen
#include <sys/socket.h>
int listen(int sockfd,int backlog);
  1. 函数应该在socket和bind之后调用,其实刚开始学习的同学可能对bind、connect和listen不太理解,bind是实现套接字和协议族的捆绑,而listen负责被动的侦听指向该套接字的连接,是从服务器端的被动行为,connect是属于客户端主动发起的行为。
  2. sockfd就是被侦听的套接字啦,backlog指定的是排队的最大连接个数,监听套接字一般有两个队列。未完成连接队列,就是三次握手尚未成功的连接,已完成队列就是已经握手完,处于连接状态。
  • accept
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *cliaddr, socklen_t *addrlen);
                    返回:成功返回非负描述符,出错返回-1
  1. 刚才listen函数提到两个队列,未完成连接和已完成连接队列,accept就是从已完成连接队列的队头返回一个连接,队列为空的时候,进程休眠。
  2. 注意!!!,这里有两个描述符,一个是作为第一个参数的监听描述符,另一个是作为返回值的由内核生成的全新的描述符。一个服务器通常仅仅创建一个监听套接字,在其声明周期一直存在。然后,内核为每一个客户创建一个已连接套接字,完成服务,此套接字关闭。

  这里给出一个返回服务器时间的小例子,我的代码是运行在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。

  • fork
      
#include <unistd.h>
pid_t fork(void);
                    返回:在子进程中为0,在父进程中为子进程ID
  1. 这个函数调用一次,返回两次,父进程就是调用进程中返回一次,返回的是子进程的ID号,而在子进程中又返回一次,返回值为0。父进程转移控制器给操作系统,操作系统会复制出一个几乎一模一样的进程来完成这个操作。
  2. 正因为如此,父子进程共享很多东西,例如所有父进程打开的描述符。通常,父进程调用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函数。

期待下一次的改进。

你可能感兴趣的:(socket,unix,网络编程)