快捷搜索:  汽车  科技

编程命令一览表(网络编程-一个简单的echo程序)

编程命令一览表(网络编程-一个简单的echo程序)//server.c //来源:公众号【编程珠玑】网站:https://www.yanbinghu.com #include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include <arpa/inet.h> #include<sys/socket.h> #include<sys/types.h> #define SERV_PORT 1234 #define MAXLINE 128 int main(int argc char **argv) { int listenfd = 0;//监听描述符 int connfd = 0; //已连接描述符 socklen_t clilen; char recvMsg

前言

在上一篇《网络编程-从TCP连接的建立说起》中简单介绍了TCP连接的建立,本文暂时先抛开TCP更加详细的介绍,来看看如何实现一个简单的网络程序。

一个简单的echo程序

本文以及后续文章都将会围绕该程序进行介绍。程序大体流程如下:

编程命令一览表(网络编程-一个简单的echo程序)(1)

首先启动服务端,客户端通过TCP的三次握手与服务端建立连接;而后,客户端发送一段字符串,服务端收到字符串后,原封不动的发回给客户端。

我们先将代码呈现,后面再进行更加详细的解释。

客户端代码client.c如下:

//client.c //来源:公众号【编程珠玑】网站:https://www.yanbinghu.com #include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include <arpa/inet.h> #include<sys/socket.h> #include<sys/types.h> #define MAXLINE 128 int main(int argc char **argv) { int sockfd; //连接描述符 struct sockaddr_in servaddr;//socket结构信息 char sendMsg[MAXLINE] = {0}; char recvMsg[MAXLINE] = {0}; //检查参数数量 if (argc < 2) { printf("usage: ./client ip port\n"); return -1; } //初始化结构体 bzero(&servaddr sizeof(servaddr)); //指定协议族 servaddr.sin_family = AF_INET; //第一个参数为ip地址,需要把ip地址转换为sin_addr类型 inet_pton(AF_INET argv[1] &servaddr.sin_addr); //第二个参数为端口号 servaddr.sin_port = htons(atoi(argv[2])); sockfd = socket(AF_INET SOCK_STREAM 0); if(-1 == sockfd) { perror("socket error"); return -1; } //连接服务器,如果非0,则连接失败 if(0 != connect(sockfd (const struct sockaddr *)&servaddr sizeof(servaddr))) { perror("connect failed"); return -1; } //从控制台读取消息 if(NULL !=fgets(sendMsg MAXLINE stdin)) { write(sockfd sendMsg strlen(sendMsg)); } if(0 != read(sockfd recvMsg MAXLINE)) { printf("recv msg:%s\n" recvMsg); } close(sockfd); return 0; }

服务端代码server.c如下:

//server.c //来源:公众号【编程珠玑】网站:https://www.yanbinghu.com #include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> #include <arpa/inet.h> #include<sys/socket.h> #include<sys/types.h> #define SERV_PORT 1234 #define MAXLINE 128 int main(int argc char **argv) { int listenfd = 0;//监听描述符 int connfd = 0; //已连接描述符 socklen_t clilen; char recvMsg[MAXLINE] = {0}; //服务器和客户端socket信息 struct sockaddr_in cliaddr servaddr; char ip[MAXLINE] = {0}; //初始化服务端socket信息 bzero(&servaddr sizeof(servaddr)); servaddr.sin_family = AF_INET; //如果输入ip和端口,使用输入的ip和端口 if(3 == argc) { inet_pton(AF_INET argv[1] &servaddr.sin_addr); servaddr.sin_port = htons(atoi(argv[2])); } else { //使用默认的ip和port servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); } listenfd = socket(AF_INET SOCK_STREAM 0); if(-1 == listenfd) { perror("socket error"); return -1; } //绑定指定ip和端口 if(0 != bind(listenfd (struct sockaddr *) &servaddr sizeof(servaddr))) { perror("bind error"); return -1; } printf("start server at %s:%d\n" inet_ntop(AF_INET &servaddr.sin_addr ip MAXLINE) ntohs(servaddr.sin_port)); listen(listenfd 4); //处理来自客户端的连接 clilen = sizeof(cliaddr); connfd = accept(listenfd (struct sockaddr *)&cliaddr &clilen); if(-1 == connfd) { perror("accept failed"); return -1; } printf("connect from %s %d\n" inet_ntop(AF_INET &cliaddr.sin_addr ip MAXLINE) ntohs(cliaddr.sin_port)); //读取客户端发送的消息 if(0 != read(connfd recvMsg MAXLINE)) { printf("recv msg:%s\n" recvMsg); } //将读取内容原封不动地发送回去 write(connfd recvMsg MAXLINE); close(connfd); close(listenfd); return 0; }

编译运行

编译客户端服务端代码:

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

在两个终端分别运行server和client。

$ ./server start server at 0.0.0.0:1234

运行客户端,并输入内容:

$ ./client 127.0.0.1 1234 hello 编程珠玑

服务端最终打印:

start server at 0.0.0.0:1234 connect from 127.0.0.1 47536 recv msg:hello 编程珠玑

客户端最终打印:

hello 编程珠玑 recv msg:hello 编程珠玑

从运行结果可以看到,客户端连接到服务端后,发送一段字符串“hello 编程珠玑”后,服务端返回同样的字符串,达到了我们想要的目的。当然代码里有很多地方还需要完善,但这不影响我们对网络编程的学习。

整体流程说明

整体流程可结合下图来理解:

编程命令一览表(网络编程-一个简单的echo程序)(2)

TCP三次握手

TCP的三次握手,我们在《网络编程-从TCP连接的建立说起》中就已经介绍了。在图中,标示了在调用某些接口后的状态。例如,服务端在调用socket,bind,listen等函数后,处于LISTEN状态;客户端调用connect函数并返回后,完成三次握手,客户端与服务端都处于ESTABLISHED状态。这些状态我们是可以观察到的,首先在一个终端启动服务器:

$ ./server start server at 0.0.0.0:1234

在另外一个终端使用netstat命令(或使用ss命令)观察:

$ netstat -anp |grep :1234 tcp 0 0 0.0.0.0:1234 0.0.0.0:* LISTEN 17730/server

netstat命令的使用可参考netstat命令详解,可以看到server程序当前处于LISTEN状态。

而如果客户端进行连接后再观察会发现:

$ netstat -anp |grep :1234 tcp 0 0 0.0.0.0:1234 0.0.0.0:* LISTEN 17730/server tcp 0 0 127.0.0.1:48094 127.0.0.1:1234 ESTABLISHED 17957/client tcp 0 0 127.0.0.1:1234 127.0.0.1:48094 ESTABLISHED 17730/server

从结果中看到,客户端此时处于ESTABLISHED状态,而服务端有一条连接处于ESTABLISHED,还有一条处于LISTEN状态,这是为何呢?我们后面再解释。

由于三次握手的过程非常快,其他的状态我们不是很方便能观察到。

那么结合代码,整个流程又是怎样的呢?请看下图:

编程命令一览表(网络编程-一个简单的echo程序)(3)

客户端-服务端

在弄清楚图中的接口含义之前,实际上你可以认为客户端连接服务器的整个过程你可以看成是这样的:

  • 服务端准备(socket,bind,listen,accept等待客户端)
  • 客户端准备(socket)
  • 客户端连接(connect)
  • 服务端收到客户端的连接(accept返回),客户端连接成功,connect返回
  • 客户端发送数据(write)
  • 服务端接收数据(read),随后又将原数据发回(write)
  • 客户端收到来自服务端的数据(read)

当然了,我们需要注意到的是:

  • 服务端在accept阻塞的过程中,处于LISTEN状态
  • 客户端在connect返回之后完成TCP的三次握手
  • 三次握手完成后,客户端与服务端处于ESTABLISHED状态
  • 服务端始终有一个处于LISTEN状态

不要着急,对于图中所提到的接口和数据结构的介绍和使用说明都会在后面进行详细介绍。

小结

看到这里,想必你对我们的echo程序的整体已经有了大致的了解。在对这些接口和数据结构进行详细介绍之前,你可以将代码复制并进行编译运行,观察文中提到的内容。

原文地址:

https://www.yanbinghu.com/2019/07/07/40135.html

数据结构与函数详解

既然要详细了解echo程序,就必须对其中用到的一些数据结构和接口有所了解。在echo程序中,我们主要用到了以下的数据结构或函数:

  • htons/ntohs
  • inet_pton/inet_ntop
  • sockaddr_in
  • socket
  • bind
  • listen
  • connect
  • accept

当然需要清楚的是,网络编程中用到的数据结构或函数远不止上面提到的这些,但这些都是最基本的。下面的解释都基于echo程序,多数函数都使用默认的阻塞模式。

htons/ntohs

htons/ntohs这两个宏分别用于将本地字节序转为网络字节序和将网络字节序转为本地字节序。关于字节序,本文不展开介绍,可以参考《谈一谈字节序的问题》,如何判断当前机器的字节序,也是面试中经常问到的题目。

inet_pton/inet_ntop

inet_pton/inet_ntop分别用于将字符串ip地址转为4字节大小的无符号整型和将无符号整型转换为ip地址字符串。例如:

//来源:公众号【编程珠玑】网站:https://www.yanbinghu.com #include<stdio.h> #include <arpa/inet.h> int main(void) { char ip[16] = "192.168.0.1"; struct in_addr addr; inet_pton(AF_INET ip &addr); printf("addr is %x\n" addr); addr.s_addr = 0x153a8c0; inet_ntop(AF_INET &addr ip sizeof(ip)); printf("ip is %s" ip); return 0; }

运行结果:

addr is 100a8c0 ip is 192.168.83.1

从运行结果中可以清晰看到两者之间的转换。需要注意的是,inet_pton/inet_ntop对IPV4和IPV6地址都适用。

sockaddr_in

sockaddr_in是IPV4套接字地址结构,它在不同系统中具体定义可能有所不同:

struct sockaddr_in{ sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; char sin_zero[8]; };

但它们都包含三个基本的成员:

  • sin_family 协议族
  • sin_port 协议端口
  • sin_addr 协议地址

协议族通常有以下几种类型:

  • AF_INET IPV4协议
  • AF_INET6 IPV6协议
  • AF_LOCAL Unix域协议
  • AF_ROUTE 路由套接字
  • AF_KEY 秘钥套接字

而目前echo程序中用到的是IPV4协议,因此选择了AF_INET。

而sin_port就比较容易理解了,它是一个16比特大小的端口,但是由于它的信息需要在网络中传输,因此需要使用前面介绍的htons进行字节序的转换。

sin_addr用4字节存储ip地址,如果是形如127.0.0.1的地址,需要通过inet_pton函数将其转换为struct in_addr类型。

socket--确定协议族和套接字类型

调用socket函数是执行网络I/O之前必须做的一件事情。通过socket函数指定了本次网络通信的协议族,套接字类型,调用成功后,会返回一个非负的套接字描述符,否则返回-1,具体失败原因,被存放于全局变量errno。它和文件描述类似,只不过此时它还不能进行正常的网络读写。

socket函数相关信息如下:

#include<sys/socket.h> int socket(int family int type int protocol);

其中family就是在介绍sockaddr_in中提到的协议族。

type通常有以下几个值:

  • SOCK_STREAM 字节流套接字
  • SOCK_DGRA 数据报套接字
  • SOCK_RAW 原始套接字
  • SOCK_SEQPACKET 有序分组套接字
  • SOCK_PACKET 分组套接字

需要注意的是:

  • TCP仅支持字节流套接字
  • UDP仅支持数据报套接字
  • SCTP支持字节流套接字和数据报套接字

protocol通常指以下几种:

  • IPPROPO_TCP TCP协议
  • IPPROPO_UDP UDP协议
  • IPPROPO_SCTP SCTP协议

通常来说,一种传输协议只支持一种套接字,此时protocol可以为0,系统会选择其对应的协议类型;否则的话,需要指定protocol的值。在当前echo程序中,type为SOCK_STREAM,我们的protocol值为0,因此使用的就是TCP协议。

我们通过一个简单的例子,观察这个套接字描述符:

//testSocket.c //来源:公众号【编程珠玑】网站:https://www.yanbinghu.com #include<stdio.h> #include <arpa/inet.h> #include<unistd.h> int main(void) { int socktfd = socket(AF_INET SOCK_STREAM 0); sleep(20); return 0; }

在一个终端运行testSocket,在另外一个终端找到该程序的pid,并查看打开的文件描述符:

$ pidof testSocket 5903 $ ls -l /proc/5903/fd/ total 0 lrwx------ 1 hyb hyb 64 7月 8 19:59 0 -> /dev/pts/6 lrwx------ 1 hyb hyb 64 7月 8 19:59 1 -> /dev/pts/6 lrwx------ 1 hyb hyb 64 7月 8 19:59 2 -> /dev/pts/6 lrwx------ 1 hyb hyb 64 7月 8 19:59 3 -> socket:[62182]

还记得那句话吗:linux下一切皆文件。

bind--指定套接字地址信息

调用socket函数之后已经确定了协议族和传输协议,但是还没有确定本地协议,即套接字地址信息。bind函数描述如下:

#include<sys/socket.h> int bind(int sockfd const struct sockaddr *addr socklen_t addrlen);

sockfd是前面调用socket函数返回的套接字描述符,用于将协议地址绑定到指定套接字中去,返回0表明成功,-1表示失败,具体失败原因,被存放于全局变量errno。addr是套接字地址,它并不是我们前面所看到的sockaddr_in类型,而是struct sockaddr,因为struct sockaddr是通用类型,不仅适用于IPV4套接字地址,也需要适用于IPV6套接字地址。

addr中的ip地址可以为0(INADDR_ANY),表示使用通配地址;而端口为0,表示由内核分配一个临时端口。服务器需要被客户端连接,因此其端口通常都是确定的,不会选择一个临时端口。

但是在客户端其ip地址和端口并非需要确切知道,因此客户端常常不绑定端口。在我们的echo程序中,我们也没有在客户端调用bind函数。

listen--监听客户端连接

listen函数用于将前面得到的套接字变为一个被动套接字,即可用于接受来自客户端的连接。描述如下:

#include<sys/socket.h> int listen(int sockfd int backlog);

返回0表明成功,-1表明失败,具体失败原因,被存放于全局变量errno。sockfd就是socket函数调用返回的套接字描述符,而backlog指明了连接队列的大小,即完成和还未完成TCP三次握手的连接总和。如果这个队列满了,服务器就不会理会新的连接请求。还记得在《网络编程-从TCP连接的建立说起》中提到的SYN攻击吗?

connect--建立连接

connect函数在客户端调用,它用来与服务端建立连接。描述如下:

#include<sys/socket.h> int connect(int sockfd const struct sockaddr *addr socklen_t addrlen);

返回0表明成功,-1表明失,具体失败原因,被存放于全局变量errno。connect函数的参数与bind函数一样,这里就不多做解释了,只不过addr指明的是远端协议地址。如果本次连接是TCP协议,则connect函数调用将会发起TCP的三次握手

accept--接受来自客户端的连接

accept函数在服务端调用,它用于接受来自客户端的连接,从已完成连接队列返回一个已完成连接。描述如下:

#include<sys/socket.h> int accept(int sockfd const struct sockaddr *addr socklen_t addrlen);

成功返回非负套接字描述符,失败返回-1,具体失败原因,被存放于全局变量errno。需要注意的是accept函数参数类型和数量与connect函数一致,但是含义不同,addr用于获取客户端的套接字地址信息,如果不关心客户端的协议地址,那么该参数可为NULL。

另外需要注意的是,它的返回值是一个非负的套接字描述符,这个套接字描述符是已连接套接字描述符,而其参数sockfd是监听套接字描述符。一个服务器通常一直有且只有一个监听套接字描述符,但通常会有多个已连接套接字描述符。还记得在上一篇中问到的吗?为什么客户端连接到服务端后,服务端有一个处于LISTEN状态,还有一个处于ESTABLISHED状态吗?

通过已连接套接字描述符就可以对其进行数据的读写了。

小结

本文主要对echo程序中用到的一些数据结构和函数进行了介绍,但没有涉及具体的异常场景,后面的文章将根据实际情况来看看其具体应用。本文常用接口总结如下:

接口 作用 成功 失败 调用者 socket 确定协议族和套接字类型 套接字描述符 -1 客户端/服务端 bind 确定套接字地址 0 -1 [客户端]/服务端 listen 套接字转为被动套接字 0 -1 服务端 connect 建立连接 0 -1 客户端 accept 接受连接 套接字描述符 -1 服务端

编程命令一览表(网络编程-一个简单的echo程序)(4)

网络编程

原文地址:

https://www.yanbinghu.com/2019/07/08/3270.html

未完待续……

如有不妥之处,欢迎批评指正。

参考书籍

  • 《Unix网络编程》
  • 《TCP/IP协议详解:卷一》

微信公众号【编程珠玑】:专注但不限于分享计算机编程基础,Linux,C语言,C ,数据结构与算法,工具,资源等编程相关[原创]技术文章。

猜您喜欢: