自己动手写http服务器(一) -- UNIX C 网络编程

系列文章:
自己动手写http服务器(一) -- UNIX C 网络编程
自己动手写http服务器(二) -- http协议分析
自己动手写http服务器(三) -- 代码实现

该系列参照开源项目 -- Tinyhttpd ;

开源项目 Tinyhttpd 只有500多行的代码,,以C语言进行编写;

linux网络编程预备知识

该文章主要介绍一下如何进行C语言的网络编程,不会全面地涉及所有的网络编程函数库,也不会对所用到函数的某个参数的所有可取值进行列举和解释,原因有二:

  • c语言中的网络编程库已经存在很久,其参数很多已经废弃,没必要说明;
  • 参数选项、功能可以随时通过查看帮助手册获得;

如果希望全面了解网络编程,可以拜读 《Unix网络编程》;

Linux网络编程的步骤

1、创建套接字

就像通过门牌号可以找到某个公司,而通过人名可以将快递准确地交给收件人一样,通过 ip地址 可以确定目标主机,通过端口号可以将数据准确地交给目标程序,而 ip地址:端口号 就是我们所说的 套接字

套接字的创建通过函数 socket ,该函数需要包含头文件 <sys/types.h><sys/socket.h> ,该函数的声明为:

//作用:创建一个套接字
//参数:
//   domain : 指定通讯协议族,常用的有 :
//      AF_INET(IPv4通讯)
//      AF_INET6(IPv6通讯)
//      AF_LOCAL(本地通讯)
//   type : 常用的有 :
//      SOCK_STREAM(有序、可靠、双向、基于连接的字节流,即TCP)
//      SOCK_DGRAM(无连接、不可靠数据报,即UDP)
//   protocol : 通常取0
//返回值
//   成功 : 返回新创建的套接字文件描述符
//   失败 : 返回 -1,错误代码存于 errno 中,通过引入 <errno.h> 可以引入该变量
int socket(int domain, int type, int protocol)

所以,如果希望套接字建立TCP连接,可以通过下面代码创建TCP套接字:

int tcp_fd = socket(AF_INET , SOCK_STREAM , 0);

如果希望套接字建立UDP连接,可以通过下面代码创建UDP套接字:

int udp_fd = socket(AF_INET , SOCK_DGRAM , 0);

2、TCP连接与通讯

无论是tcp还是udp,第一步都需要创建套接字,而之后的操作差异较大,TCP在数据收发前,需要建立连接,而UDP不需要建立连接就可以收发数据。

(1) 建立TCP server的步骤

TCP服务器的设置步骤如下:

  1. 通过 socket() 系统调用创建一个套接字;
  2. 使用 bind() 系统调用将所创建的套接字绑定到指定的端口上;
  3. 通过 listen() 将进行端口绑定的套接字进行端口侦听,使客户端能够连接;
  4. 通过 accept() 接受客户端的连接,该函数将会被阻塞,直至客户端连接上来;
  5. 数据收发 read / write

注意:如果在bind绑定时,指定端口0,意味着由系统随机选择一个可用端口来绑定;

服务端代码示例:

// file name : server.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>  //定义了大量系统调用需要使用的类型
#include <sys/socket.h>  //定义了大量套接字所需要的结构体
#include <netinet/in.h>  //与网络相关的结构体及函数

// 该函数将字符串 msg 输出到 stderr,并退出执行
void error(char* msg){
  fprintf(stderr, "%s\n", msg);
  exit(1);
}
int main(int argc , char* argv[]){
  int sockfd;   //保存服务器套接字的文件描述符
  int newsockfd;   //保存与客户端通讯套接字的文件描述符
  int portno;   //保存服务器要绑定的端口号
  int clilen;   //保存client地址的大小,系统调用时需要使用
  int n;   //存放函数read、write的返回值
  char buffer[256];   //服务器将从套接字中读取的字符存入该缓存
  struct sockaddr_in serv_addr, cli_addr ; //存放服务器、客户端套接字属性
  if(argc < 2) error("Error : no port provided.\n");
  sockfd = socket(AF_INET , SOCK_STREAM , 0); //创建TCP套接字
  if(sockfd < 0) error("Error : fail to open socket");
  memset(&serv_addr , 0 , sizeof(serv_addr));
  portno = atoi(argv[1]) ;  //将输入的第二个参数转换为端口号

  // 配置服务器参数
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_port = htons(portno);
  serv_addr.sin_addr.s_addr = INADDR_ANY;  //本机地址 0

  if(bind(sockfd, (const struct sockaddr*)&serv_addr, 
    sizeof(serv_addr) ) < 0){
    error("Error : binding");
  }
  listen(sockfd, 5);  //进行端口侦听
  clilen = sizeof(cli_addr);
  //阻塞等待客户端的连接
  newsockfd = accept(sockfd, (struct sockaddr*) &cli_addr, &clilen);
  if(newsockfd < 0){
    error("Error on accept");
  }
  memset(buffer, 0, 256);
  n = read(newsockfd, buffer, 255);
  if(n < 0) error("Error reading from socket");
  printf("client message : %s\n" , buffer);
  n = write(newsockfd, "I got your message", 18);
  if(n < 0)error("Error writing to socket");
  close(newsockfd);
  close(sockfd);
  return 0;
}

说明:什么是文件描述符?

每一个运行的进程都有一个文件描述符表(file descriptor table),该表中存放了所有指向已经打开的 i/o 流;

当一个进程启动之后,默认将3个指针添加到文件描述符表中,0指向标准输入stdin,1指向标准输出stdout,2指向错误输出stderr;每当有一个文件被打开后,就会有一个入口指针被创建;文件描述符是一个短整型(16 bit),可以是文件,但不限于文件,也可以是其他能够读写的对象,如套接字、管道、内存块等;

通过 open 函数可以打开一个文件,并返回文件描述符;readwrite 函数需要传入文件描述符,对指定对象进行读写;

说明:sockaddr_in的作用?

sockaddr_in 是一个包含网络地址的结构体,该结构体在 <netinet/in.h> 头文件中进行定义:

struct sockaddr_in{
  short sin_family;
  u_short sin_port;
  struct in_addr sin_addr;
  char sin_zero[8];
};

其中,in_addr 结构体也定义在 <netinet/in.h> 中,用于存放ip地址:

struct in_addr{
  unsigned long s_addr;
};

该结构体主要用于存放网络相关的属性信息,在 bind 时指定需要绑定的套接字;在 sendto 时指定需要发送到的对象;

(2) 建立TCP client的步骤

相对于TCP server 的建立,TCP client 的建立过程较为简单:

  1. 通过 socket() 系统调用创建一个套接字;
  2. 通过 connect() 系统调用将创建的套接字连接到TCP服务器上;
  3. 数据收发;数据收发的方式有很多,其中最简单的方式是使用系统调用 read()write() 进行数据收发;

客户端代码示例:

// file name : client.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

void error(char* msg){
  fprintf(stderr, "%s\n", msg);
  exit(1);
}

int main(int argc, char* argv[]){
  int sockfd, portno, n;
  struct sockaddr_in serv_addr;
  char buffer[256] = {0};

  if(argc < 3){
    // 使用客户端的方式为 :执行文件名   主机名或IP   端口号
    fprintf(stderr, "usage %s hostname port\n", argv[0]);
    exit(1);
  }
  portno = atoi(argv[2]);
  // 创建套接字
  sockfd = socket(AF_INET, SOCK_STREAM, 0);  
  if(sockfd == NULL) exit("Error opening socket");
  
  // 设置待连接服务器参数
  memset(&serv_addr, 0, sizeof(serv_addr));
  serv_addr.sin_family = AF_INET ;
  serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
  serv_addr.sin_port = htons(portno);
  
  // 连接指定服务器
  if(connect(sockfd, &serv_addr, sizeof(serv_addr)) < 0)
    error("Error connecting");
  printf("Please enter the massage : ");
  fgets(buffer, 255, stdin);
  // 向服务器发送数据
  n = write(sockfd, buffer, strlen(buffer));
  if(n < 0) error("Error writing to socket");
  memset(buffer, 0, 256);
  // 读取服务器发送过来的数据
  n = read(sockfd, buffer, 255);
  if(n < 0)
    error("Error reading from socket");
  printf("%s\n", buffer);
  return 0;
}
(3)编译与运行

通过命令:

  gcc server.c -o server
  gcc client.c -o client

编译出服务器和客户端程序;先打开 server,指定端口为7777,再打开 client,连接到服务器,即可进行数据通讯,实验结果如下:

TCP通讯

UDP连接与通讯

UDP并不是基于连接的数据通讯,也就是说UDP server 并不通过 accept 接收客户端的连接,而UDP client 也不通过 connect 连接到服务器;

(1)UDP 服务器

要创建UDP服务器需要如下三步:

  1. 创建套接字 (socket)
  2. 绑定端口 (bind)
  3. 数据通讯 (读read / 写write)

与TCP服务器相比,少了 listenaccept 两个过程,即建立连接的两个步骤;

下面的代码是一个简单的 “回音” 服务器,即 udp客户端发送的内容将被udp服务器发回:

// file name : udpserver.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUFLEN 512    //数据缓冲区的大小

void error(const char* message){
  fprintf(stderr, "%s\n", message);
  exit(1);
}

int main(int argc, char* argv[]){
  int server_fd ;
  int recv_len ;   // 保存接收到的数据长度
  char buf[BUFLEN] = {0}; //保存接收到的数据
  struct sockaddr_in si_me, si_other;
  int slen = sizeof(si_other);
  
  if(argc < 2){
    fprintf(stderr, "USAGE : %s  port\n", argv[0]);
    exit(1);
  }
  // 创建套接字,指明使用的是数据包协议
  if((server_fd=socket(AF_INET, SOCK_DGRAM, 0))==-1){
    error("Error to create socket");
  }
  // 设置服务器参数
  memset(&si_me, 0, sizeof(si_me));
  si_me.sin_family = AF_INET;
  si_me.sin_port = htons( atoi(argv[1]) );  
  si_me.sin_addr.s_addr = htonl(INADDR_ANY);
  // 将udp套接字绑定到指定端口
  if(bind(server_fd, &si_me, sizeof(si_me)) == -1){
    error("Error when bind to port");
  }
  // 循环:接收数据并写回
  while(1){
    memset(buf, 0, BUFLEN);
    printf("Waiting from data ...\n");
    if( (recv_len = recvfrom(server_fd, buf, BUFLEN-1, 0, 
      &si_other, &slen)) == -1){
      error("Error receive from udp client");
    }
    printf("Receive : %s\n", buf);
    if(sendto(server_fd, buf, strlen(buf), 0, &si_other, slen) == -1){
      error("Error send to udp client");
    }
  }
  close(server_fd);
  return 0;
}
(2)UDP客户端

创建UDP客户端需要如下几步:

  1. 创建套接字 (socket)
  2. 数据通讯 (读recvfrom / 写sendto)

与TCP客户端相比,少了连接 connect 步骤,即不需要建立连接,直接进行数据收发;

下面代码表示将命令行收到的数据通过UDP协议发送给服务器:

// file name : udpclient.c
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

#define BUFLEN 512

void error(const char* message){
  fprintf(stderr, "%s\n", message);
  exit(1);
}

int main(int argc, char* argv[]){
  int clientSocket, portNum, nBytes;
  char buffer[BUFLEN];
  struct sockaddr_in serverAddr;
  socklen_t addr_size;

  if(argc < 3){
    fprintf(stderr, "USAGE : %s  ip  port \n", argv[0]);
    exit(1);
  }
  // 创建客户端套接字
  if((clientSocket = socket(PF_INET, SOCK_DGRAM, 0))==-1){
    error("Error to create udp socket");
  }

  // 配置连接属性
  serverAddr.sin_family = AF_INET;
  serverAddr.sin_port = htons(atoi(argv[2]));
  serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
  memset(serverAddr.sin_zero, '\0', sizeof serverAddr.sin_zero);  
  addr_size = sizeof serverAddr;

  while(1){
    printf("请输入发送数据:");
    fgets(buffer,BUFLEN,stdin);
    nBytes = strlen(buffer) + 1;
    
    /*将命令行收到的数据发送到服务器端*/
    sendto(clientSocket,buffer,nBytes,0,&serverAddr,addr_size);

    /*接收从服务器端发送来的数据*/
    nBytes = recvfrom(clientSocket,buffer,BUFLEN,0,NULL, NULL);
    printf("接收数据: %s\n",buffer);
  }
  return 0;
}
(3)编译与运行

通过命令:

  gcc udpserver.c -o udpserver
  gcc udpclient.c -o udpclient

编译后,udpserver 是 "回声" 服务器, udpclient 是udp数据接收器,实验结果如下:

UDP通讯

参考内容

Sockets Tutorial

Programming udp sockets in C on Linux

UDP made simple

完!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容

  • 参考:http://www.2cto.com/net/201611/569006.html TCP HTTP UD...
    F麦子阅读 2,945评论 0 14
  • 网络概念第一天 两台电脑怎么通过网络传输数据?怎样才能知道传输的是数据?谁摸过网线? 看电影,怎么看的?通过电流,...
    小吖朱阅读 1,549评论 0 1
  • 1. 网络编程概述 1.1 计算机网络 是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接...
    JackChen1024阅读 1,033评论 0 3
  • 泉城的初冬 文/鲁先圣 今年泉城的冬天与往年不同,她自有独特的韵味,不像北方的其它城市那样凛冽的寒冷,也不像南...
    鲁先圣阅读 541评论 0 2
  • (写作于2013年4月27日) 中国传统文化的老祖宗孔夫子曰:“知之者不如好之者,好之者不如乐之者。” 意...
    万月生阅读 300评论 0 2