通过 TCP 协议进行 C/S 模式的网络通信
学习要由浅入深, 由易到难, 分析 Linux 内核中网络部分就要从内核对外提供的 socket 封装接口说起, 典型以 TCP 协议 C/S 方式 socket 通信大致过程如图所示:
(图片来源于网络)
从图中可以看到 TCP 服务端 server 的初始化过程复杂一些, 就像开一个小卖铺, 你要登记为个体工商户其中最重要的就是营业地址 (也就是 bind 绑定 IP 地址和端口号), 然后就可以开门营业了 (listen), 营业需要有营业员在那等着接待客户 (也就是 accept), 这样就完成了 TCP 服务端 server 的初始化.
TCP 客户端 client 的初始化比较简单一些, 就像你要去小卖铺买东西, 你只要知道小卖铺的营业地址 (IP 地址和端口号), 就可以去买东西了 (connect).
客户端 connect 服务端 accept 对接上了, 客户和营业员就可以谈生意, 你一句我一句 (send 和 recv), 达成交易客户端 close 离场, 服务端继续等着接待客户 (也就是 accept).
服务端代码
接下来以一个简单代码 hello/hi 范例来具体了解 TCP 协议 C/S 方式 socket 通信代码.
首先看服务端程序代码, 来一个客户就 reply hi.
- #include"syswrapper.h"
- #define MAX_CONNECT_QUEUE 1024
- int main()
- {
- char szBuf[MAX_BUF_LEN] = "\0";
- char szReplyMsg[MAX_BUF_LEN] = "hi\0";
- InitializeService();
- while(1)
- {
- ServiceStart();
- RecvMsg(szBuf);
- SendMsg(szReplyMsg);
- ServiceStop();
- }
- ShutdownService();
- return 0;
- }
客户端代码
然后看客户端程序代码, 发送 hello, 接收 hi.
- #include"syswrapper.h"
- #define MAX_CONNECT_QUEUE 1024
- int main()
- {
- char szBuf[MAX_BUF_LEN] = "\0";
- char szMsg[MAX_BUF_LEN] = "hello\0";
- OpenRemoteService();
- SendMsg(szMsg);
- RecvMsg(szBuf);
- CloseRemoteService();
- return 0;
- }
socket 接口封装代码
以上客户端和服务端代码我们都做了简单的封装, 实际上看不到具体的 socket 代码, 具体用到 socket 接口的代码如下:
- /********************************************************************/
- /* Copyright (C) SSE-USTC, 2012 */
- /* */
- /* FILE NAME : syswraper.h */
- /* PRINCIPAL AUTHOR : Mengning */
- /* SUBSYSTEM NAME : system */
- /* MODULE NAME : syswraper */
- /* LANGUAGE : C */
- /* TARGET ENVIRONMENT : Linux */
- /* DATE OF FIRST RELEASE : 2012/11/22 */
- /* DESCRIPTION : the interface to Linux system(socket) */
- /********************************************************************/
- /*
- * Revision log:
- *
- * Created by Mengning,2012/11/22
- *
- */
- #ifndef _SYS_WRAPER_H_
- #define _SYS_WRAPER_H_
- #include<stdio.h>
- #include<arpa/.NET.h> /* internet socket */
- #include<string.h>
- //#define NDEBUG
- #include<assert.h>
- #define PORT 5001
- #define IP_ADDR "127.0.0.1"
- #define MAX_BUF_LEN 1024
- /* private macro */
- #define PrepareSocket(addr,port) \
- int sockfd = -1; \
- struct sockaddr_in serveraddr; \
- struct sockaddr_in clientaddr; \
- socklen_t addr_len = sizeof(struct sockaddr); \
- serveraddr.sin_family = AF_INET; \
- serveraddr.sin_port = htons(port); \
- serveraddr.sin_addr.s_addr = inet_addr(addr); \
- memset(&serveraddr.sin_zero, 0, 8); \
- sockfd = socket(PF_INET,SOCK_STREAM,0);
- #define InitServer() \
- int ret = bind( sockfd, \
- (struct sockaddr *)&serveraddr, \
- sizeof(struct sockaddr)); \
- if(ret == -1) \
- {
- \
- fprintf(stderr,"Bind Error,%s:%d\n", \
- __FILE__,__LINE__); \
- close(sockfd); \
- return -1; \
- } \
- listen(sockfd,MAX_CONNECT_QUEUE);
- #define InitClient() \
- int ret = connect(sockfd, \
- (struct sockaddr *)&serveraddr, \
- sizeof(struct sockaddr)); \
- if(ret == -1) \
- {
- \
- fprintf(stderr,"Connect Error,%s:%d\n", \
- __FILE__,__LINE__); \
- return -1; \
- }
- /* public macro */
- #define InitializeService() \
- PrepareSocket(IP_ADDR,PORT); \
- InitServer();
- #define ShutdownService() \
- close(sockfd);
- #define OpenRemoteService() \
- PrepareSocket(IP_ADDR,PORT); \
- InitClient(); \
- int newfd = sockfd;
- #define CloseRemoteService() \
- close(sockfd);
- #define ServiceStart() \
- int newfd = accept( sockfd, \
- (struct sockaddr *)&clientaddr, \
- &addr_len); \
- if(newfd == -1) \
- {
- \
- fprintf(stderr,"Accept Error,%s:%d\n", \
- __FILE__,__LINE__); \
- }
- #define ServiceStop() \
- close(newfd);
- #define RecvMsg(buf) \
- ret = recv(newfd,buf,MAX_BUF_LEN,0); \
- if(ret> 0) \
- {
- \
- printf("recv \"%s\"from %s:%d\n", \
- buf, \
- (char*)inet_ntoa(clientaddr.sin_addr), \
- ntohs(clientaddr.sin_port)); \
- }
- #define SendMsg(buf) \
- ret = send(newfd,buf,strlen(buf),0); \
- if(ret> 0) \
- {
- \
- printf("rely \"hi\"to %s:%d\n", \
- (char*)inet_ntoa(clientaddr.sin_addr), \
- ntohs(clientaddr.sin_port)); \
- }
- #endif /* _SYS_WRAPER_H_ */
这里通过宏定义的方式对 socket 接口做了简单的封装, 封装起来有两个好处: 一是把所有和 socket 有关的代码放在一起便于维护和移植, 另一个是使得上层代码的业务过程更清晰. 当然这里与我们理解 socket 接口的关系不太大, 能理解 socket 的通信过程就好.
这段代码里涉及了 socket 接口的相关内容, 比如网络地址的结构体变量, socket 函数及其参数等, 需要我们仔细研究了解他们的具体作用.
sockaddr 和 sockaddr_in 的不同作用
一般在 Linux 环境下 / usr/include/bits/socket.h 或 / usr/include/sys/socket.h 可以看到 sockaddr 的结构体声明.
- /* Structure describing a generic socket address. */
- struct sockaddr
- {
- __SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
- char sa_data[14]; /* Address data. */
- };
这是一个通用的 socket 地址可以兼容不同的协议, 当然包括基于 TCP/IP 的互联网协议, 为了方便起见互联网 socket 地址的结构提供定义的更具体见 / usr/include/netinet/in.h 文件中的 struct sockaddr_in.
- /* Structure describing an Internet socket address. */
- struct sockaddr_in
- {
- __SOCKADDR_COMMON (sin_);
- in_port_t sin_port; /* Port number. */
- struct in_addr sin_addr; /* Internet address. */
- /* Pad to size of `struct sockaddr'. */
- unsigned char sin_zero[sizeof (struct sockaddr) -
- __SOCKADDR_COMMON_SIZE -
- sizeof (in_port_t) -
- sizeof (struct in_addr)];
- };
sockaddr 和 sockaddr_in 的关系有点像面向对象编程中的父类和子类, 子类重新定义了父类的地址数据格式. 同一块数据我们根据需要使用两个不同的结构体变量来存取数据内容, 这也是最简单的面向对象编程中的多态特性的实现方法.
AF_INET 和 PF_INET
在 / usr/include/bits/socket.h 或 / usr/include/sys/socket.h 中一般可以找到 AF_INET 和 PF_INET 的宏定义如下.
- /* Protocol families. */
- ...
- #define PF_INET 2 /* IP protocol family. */
- ...
- /* Address families. */
- ...
- #define AF_INET PF_INET
- ...
尽管他们的值相同, 但它们的含义是不同的, 网上很多代码将 AF_INET 和 PF_INET 混用, 如果您了解他们的含义就不会随便混用了, 根据如下注释可以看到 A 代表 Address families,P 代表 Protocol families, 也就是说当表示地址时用 AF_INET, 表示协议时用 PF_INET. 参见我们实验室代码中的使用方法,"serveraddr.sin_family = AF_INET;" 中使用 AF_INET, 而 "sockfd = socket(PF_INET,SOCK_STREAM,0);" 中使用 PF_INET.
SOCK_STREAM 及其他协议
在 / usr/include/bits/socket_type.h 可以找到 "__socket_type", 不同协议族一般都会定义不同的类型的通信方式, 对于基于 TCP/IP 的互联网协议族 (即 PF_INET), 面向连接的 TCP 协议的 socket 类型即为 SOCK_STREAM, 无连接的 UDP 协议即为 SOCK_DGRAM, 而 SOCK_RAW 工作在网络层. SOCK_RAW 可以处理 ICMP,IGMP 等网络报文, 特殊的 IPv4 报文等.
- /* Types of sockets. */
- enum __socket_type
- {
- SOCK_STREAM = 1, /* Sequenced, reliable, connection-based
- byte streams. */
- #define SOCK_STREAM SOCK_STREAM
- SOCK_DGRAM = 2, /* Connectionless, unreliable datagrams
- of fixed maximum length. */
- #define SOCK_DGRAM SOCK_DGRAM
- SOCK_RAW = 3, /* Raw protocol interface. */
- #define SOCK_RAW SOCK_RAW
- SOCK_RDM = 4, /* Reliably-delivered messages. */
- #define SOCK_RDM SOCK_RDM
- SOCK_SEQPACKET = 5, /* Sequenced, reliable, connection-based,
- datagrams of fixed maximum length. */
- ...
如上几点对于我们后续进一步理解和分析 Linux 网络代码比较重要, 代码中涉及的其他接口及参数可以在实验过程中自行查阅相关资料.
实验指导
本实验环境见
以上代码可以 clone linuxnet.Git 并参照如下指令编译执行代码:
- shiyanlou:~/ $ cd cd LinuxKernel
- shiyanlou:Code/ $ Git clone
- shiyanlou:Code/ $ cd linuxnet
- shiyanlou:linuxnet/ (master) $ cd lab1
- shiyanlou:lab1/ (master) $ ls
- client.c server.c syswrapper.h
- shiyanlou:lab1/ (master) $ make
- shiyanlou:lab1/ (master*) $ ./server
- recv "hello" from 127.0.0.1:58911
- send "hi" to 127.0.0.1:58911
右击水平分割 Xfce 终端 (Terminal), 执行 client
- shiyanlou:lab1/ (master*) $ ./client
- send "hi" to 0.0.0.0:60702
- recv "hi" from 0.0.0.0:60702
- shiyanlou:lab1/ (master*) $
本博文摘取自专栏《庖丁解牛 Linux 网络核心》, 现在订阅, 抢 200 个早鸟名额!
专栏说明
首先声明本专栏的目标并不是帮助大家获得立即可能使用的专业技能, 而是希望能通过研究分析 Linux 内核中网络部分的代码实现来深刻理解互联网运作的核心机制, 看完本专栏预期可以达成如下目标:
从整体上理解互联网运作的方式;
能分析上网打开一个网页的过程中互联网底层具体做了哪些工作, 从而在遇到网络相关问题时能独立分析定位问题;
由于我们涉及的实验都是在 Linux 系统完成的, 您还会进一步熟悉 Linux 系统;
分析 Linux 内核中网络部分当然也少不了对网络协议及 RFC 文档的讨论, 相信您也能对网络标准有更多的了解.
来源: http://blog.51cto.com/4119965/2314005