系列文章:
自己动手写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服务器的设置步骤如下:
- 通过 socket() 系统调用创建一个套接字;
- 使用 bind() 系统调用将所创建的套接字绑定到指定的端口上;
- 通过 listen() 将进行端口绑定的套接字进行端口侦听,使客户端能够连接;
- 通过 accept() 接受客户端的连接,该函数将会被阻塞,直至客户端连接上来;
- 数据收发 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 函数可以打开一个文件,并返回文件描述符;read 和 write 函数需要传入文件描述符,对指定对象进行读写;
说明: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 的建立过程较为简单:
- 通过 socket() 系统调用创建一个套接字;
- 通过 connect() 系统调用将创建的套接字连接到TCP服务器上;
- 数据收发;数据收发的方式有很多,其中最简单的方式是使用系统调用 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服务器需要如下三步:
- 创建套接字 (socket)
- 绑定端口 (bind)
- 数据通讯 (读read / 写write)
与TCP服务器相比,少了 listen 和 accept 两个过程,即建立连接的两个步骤;
下面的代码是一个简单的 “回音” 服务器,即 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客户端需要如下几步:
- 创建套接字 (socket)
- 数据通讯 (读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通讯
参考内容
Programming udp sockets in C on Linux
完!
作者:FoolishFlyFox
链接:https://www.jianshu.com/p/c2e3a05e7cae
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。