自己动手写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> ,该函数的声明为:


   
   
  1. //作用:创建一个套接字
  2. //参数:
  3. // domain : 指定通讯协议族,常用的有 :
  4. // AF_INET(IPv4通讯)
  5. // AF_INET6(IPv6通讯)
  6. // AF_LOCAL(本地通讯)
  7. // type : 常用的有 :
  8. // SOCK_STREAM(有序、可靠、双向、基于连接的字节流,即TCP)
  9. // SOCK_DGRAM(无连接、不可靠数据报,即UDP)
  10. // protocol : 通常取0
  11. //返回值
  12. // 成功 : 返回新创建的套接字文件描述符
  13. // 失败 : 返回 -1,错误代码存于 errno 中,通过引入 <errno.h> 可以引入该变量
  14. 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,意味着由系统随机选择一个可用端口来绑定;

服务端代码示例:


   
   
  1. // file name : server.c
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. #include <unistd.h>
  6. #include <sys/types.h> //定义了大量系统调用需要使用的类型
  7. #include <sys/socket.h> //定义了大量套接字所需要的结构体
  8. #include <netinet/in.h> //与网络相关的结构体及函数
  9. // 该函数将字符串 msg 输出到 stderr,并退出执行
  10. void error(char* msg){
  11. fprintf( stderr, "%s\n", msg);
  12. exit( 1);
  13. }
  14. int main(int argc , char* argv[]){
  15. int sockfd; //保存服务器套接字的文件描述符
  16. int newsockfd; //保存与客户端通讯套接字的文件描述符
  17. int portno; //保存服务器要绑定的端口号
  18. int clilen; //保存client地址的大小,系统调用时需要使用
  19. int n; //存放函数read、write的返回值
  20. char buffer[ 256]; //服务器将从套接字中读取的字符存入该缓存
  21. struct sockaddr_in serv_addr, cli_addr ; //存放服务器、客户端套接字属性
  22. if(argc < 2) error( "Error : no port provided.\n");
  23. sockfd = socket(AF_INET , SOCK_STREAM , 0); //创建TCP套接字
  24. if(sockfd < 0) error( "Error : fail to open socket");
  25. memset(&serv_addr , 0 , sizeof(serv_addr));
  26. portno = atoi(argv[ 1]) ; //将输入的第二个参数转换为端口号
  27. // 配置服务器参数
  28. serv_addr.sin_family = AF_INET;
  29. serv_addr.sin_port = htons(portno);
  30. serv_addr.sin_addr.s_addr = INADDR_ANY; //本机地址 0
  31. if(bind(sockfd, ( const struct sockaddr*)&serv_addr,
  32. sizeof(serv_addr) ) < 0){
  33. error( "Error : binding");
  34. }
  35. listen(sockfd, 5); //进行端口侦听
  36. clilen = sizeof(cli_addr);
  37. //阻塞等待客户端的连接
  38. newsockfd = accept(sockfd, (struct sockaddr*) &cli_addr, &clilen);
  39. if(newsockfd < 0){
  40. error( "Error on accept");
  41. }
  42. memset(buffer, 0, 256);
  43. n = read(newsockfd, buffer, 255);
  44. if(n < 0) error( "Error reading from socket");
  45. printf( "client message : %s\n" , buffer);
  46. n = write(newsockfd, "I got your message", 18);
  47. if(n < 0)error( "Error writing to socket");
  48. close(newsockfd);
  49. close(sockfd);
  50. return 0;
  51. }

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

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

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

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

说明:sockaddr_in的作用?

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


   
   
  1. struct sockaddr_in{
  2. short sin_family;
  3. u_short sin_port;
  4. struct in_addr sin_addr;
  5. char sin_zero[ 8];
  6. };

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


   
   
  1. struct in_addr{
  2. unsigned long s_addr;
  3. };

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

(2) 建立TCP client的步骤

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

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

客户端代码示例:


   
   
  1. // file name : client.c
  2. #include <stdio.h>
  3. #include <sys/types.h>
  4. #include <sys/socket.h>
  5. #include <netinet/in.h>
  6. void error(char* msg){
  7. fprintf( stderr, "%s\n", msg);
  8. exit( 1);
  9. }
  10. int main(int argc, char* argv[]){
  11. int sockfd, portno, n;
  12. struct sockaddr_in serv_addr;
  13. char buffer[ 256] = { 0};
  14. if(argc < 3){
  15. // 使用客户端的方式为 :执行文件名 主机名或IP 端口号
  16. fprintf( stderr, "usage %s hostname port\n", argv[ 0]);
  17. exit( 1);
  18. }
  19. portno = atoi(argv[ 2]);
  20. // 创建套接字
  21. sockfd = socket(AF_INET, SOCK_STREAM, 0);
  22. if(sockfd == NULL) exit( "Error opening socket");
  23. // 设置待连接服务器参数
  24. memset(&serv_addr, 0, sizeof(serv_addr));
  25. serv_addr.sin_family = AF_INET ;
  26. serv_addr.sin_addr.s_addr = inet_addr(argv[ 1]);
  27. serv_addr.sin_port = htons(portno);
  28. // 连接指定服务器
  29. if(connect(sockfd, &serv_addr, sizeof(serv_addr)) < 0)
  30. error( "Error connecting");
  31. printf( "Please enter the massage : ");
  32. fgets(buffer, 255, stdin);
  33. // 向服务器发送数据
  34. n = write(sockfd, buffer, strlen(buffer));
  35. if(n < 0) error( "Error writing to socket");
  36. memset(buffer, 0, 256);
  37. // 读取服务器发送过来的数据
  38. n = read(sockfd, buffer, 255);
  39. if(n < 0)
  40. error( "Error reading from socket");
  41. printf( "%s\n", buffer);
  42. return 0;
  43. }

(3)编译与运行

通过命令:


   
   
  1. gcc server.c -o server
  2. 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服务器发回:


   
   
  1. // file name : udpserver.c
  2. #include <stdio.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. #include <arpa/inet.h>
  6. #include <sys/socket.h>
  7. #define BUFLEN 512 //数据缓冲区的大小
  8. void error(const char* message){
  9. fprintf( stderr, "%s\n", message);
  10. exit( 1);
  11. }
  12. int main(int argc, char* argv[]){
  13. int server_fd ;
  14. int recv_len ; // 保存接收到的数据长度
  15. char buf[BUFLEN] = { 0}; //保存接收到的数据
  16. struct sockaddr_in si_me, si_other;
  17. int slen = sizeof(si_other);
  18. if(argc < 2){
  19. fprintf( stderr, "USAGE : %s port\n", argv[ 0]);
  20. exit( 1);
  21. }
  22. // 创建套接字,指明使用的是数据包协议
  23. if((server_fd=socket(AF_INET, SOCK_DGRAM, 0))== -1){
  24. error( "Error to create socket");
  25. }
  26. // 设置服务器参数
  27. memset(&si_me, 0, sizeof(si_me));
  28. si_me.sin_family = AF_INET;
  29. si_me.sin_port = htons( atoi(argv[ 1]) );
  30. si_me.sin_addr.s_addr = htonl(INADDR_ANY);
  31. // 将udp套接字绑定到指定端口
  32. if(bind(server_fd, &si_me, sizeof(si_me)) == -1){
  33. error( "Error when bind to port");
  34. }
  35. // 循环:接收数据并写回
  36. while( 1){
  37. memset(buf, 0, BUFLEN);
  38. printf( "Waiting from data ...\n");
  39. if( (recv_len = recvfrom(server_fd, buf, BUFLEN -1, 0,
  40. &si_other, &slen)) == -1){
  41. error( "Error receive from udp client");
  42. }
  43. printf( "Receive : %s\n", buf);
  44. if(sendto(server_fd, buf, strlen(buf), 0, &si_other, slen) == -1){
  45. error( "Error send to udp client");
  46. }
  47. }
  48. close(server_fd);
  49. return 0;
  50. }

(2)UDP客户端

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

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

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

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


   
   
  1. // file name : udpclient.c
  2. #include <stdio.h>
  3. #include <sys/socket.h>
  4. #include <netinet/in.h>
  5. #include <string.h>
  6. #define BUFLEN 512
  7. void error(const char* message){
  8. fprintf( stderr, "%s\n", message);
  9. exit( 1);
  10. }
  11. int main(int argc, char* argv[]){
  12. int clientSocket, portNum, nBytes;
  13. char buffer[BUFLEN];
  14. struct sockaddr_in serverAddr;
  15. socklen_t addr_size;
  16. if(argc < 3){
  17. fprintf( stderr, "USAGE : %s ip port \n", argv[ 0]);
  18. exit( 1);
  19. }
  20. // 创建客户端套接字
  21. if((clientSocket = socket(PF_INET, SOCK_DGRAM, 0))== -1){
  22. error( "Error to create udp socket");
  23. }
  24. // 配置连接属性
  25. serverAddr.sin_family = AF_INET;
  26. serverAddr.sin_port = htons(atoi(argv[ 2]));
  27. serverAddr.sin_addr.s_addr = inet_addr(argv[ 1]);
  28. memset(serverAddr.sin_zero, '\0', sizeof serverAddr.sin_zero);
  29. addr_size = sizeof serverAddr;
  30. while( 1){
  31. printf( "请输入发送数据:");
  32. fgets(buffer,BUFLEN, stdin);
  33. nBytes = strlen(buffer) + 1;
  34. /*将命令行收到的数据发送到服务器端*/
  35. sendto(clientSocket,buffer,nBytes, 0,&serverAddr,addr_size);
  36. /*接收从服务器端发送来的数据*/
  37. nBytes = recvfrom(clientSocket,buffer,BUFLEN, 0, NULL, NULL);
  38. printf( "接收数据: %s\n",buffer);
  39. }
  40. return 0;
  41. }

(3)编译与运行

通过命令:


   
   
  1. gcc udpserver.c -o udpserver
  2. gcc udpclient.c -o udpclient

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

UDP通讯

参考内容

Sockets Tutorial

Programming udp sockets in C on Linux

UDP made simple

完!



作者:FoolishFlyFox
链接:https://www.jianshu.com/p/c2e3a05e7cae
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值