院 系:
班 级:
姓 名: X X X()
合 作 者: X X X() X X X()
指导教师:
2015 年 12 月 25 日
网络程序设计课程设计任务书
一、题目:X X X系统
二、设计要求
(1)XXX(组长)、XXX和XXX组成设计小组。
(2)小组成员分工协作完成,要求每个成员有自己相对独立的模块,同时要了解其他组员完成的内容。
(3)查阅相关资料,自学具体课题中涉及到的新知识。
(4)根据实现的功能,划分出合理的模块,明确UDP通信原理和方法,及完成客户端程序与服务器间的通信功能。
(5)编程简练,程序功能齐全,能正确运行。
(6)课题完成后提交课程设计报告,格式规范,内容详实。其主要内容包括:1-封皮、2-课程设计任务书,3-指导教师评语与成绩、4-目录、5-需求分析、6-概要设计、7-详细设计(含主要代码)、8-调试分析与测试结果、9-用户使用说明、10-附录或参考资料。报告用A4纸打印,中文字体为宋体,西文字体用Time New Roma,小四号字,行距采用“固定值”18磅,首行缩进2字符。
三、课程设计工作量
由于是设计小组团结协作完成设计任务,一般每人的程序量在200行有效程序行左右,不得抄袭。
四、课程设计工作计划
20##年12月14日,指导教师讲课,学生根据题目准备资料;
20##年12月15日、16日、17日,设计小组进行总体方案设计和任务分工;每人完成自己承担的程序模块并通过独立编译;
20##年12月18日~20##年12月24日,将各模块集成为一个完整的系统,并录入足够的数据进行调试运行;同时撰写报告。
20##年12月25日,验收;提交课程设计报告。
指导教师签章: 教研室主任签章
网络程序设计课程设计指导教师评语与成绩
目 录
第1章 需求分析 1
1.1 性能需求 1
1.2 功能需求 1
第2章 概要设计 2
2.1 功能模块设计 2
2.2 涉及的类及方法 2
第3章 详细设计 3
3.1 服务器程序功能模块设计 3
3.2 客户端程序功能模块设计 3
第4章 调试分析与测试结果 4
4.1 调试分析 4
4.2 测试结果 4
第5章 用户使用说明 5
参考文献 6
附 录 7
第1章 需求分析
1.1 性能需求
(软、硬件开发环境的需求,要完成课题需要查阅的资料或者调研的内容)
1.2 功能需求
(描述课题所实现的几个功能)
……
第2章 概要设计
2.1 功能模块设计
(功能模块图和描述)
2.2 涉及的类与方法
(类和方法的描述)
第3章 详细设计
3.1 服务器程序功能模块设计
(详细功能描述;设计思想;程序流程图;主要代码)
3.2 客户端程序功能模块设计
(详细功能描述;设计思想;程序流程图;主要代码)
……
第4章 调试分析与测试结果
4.1 调试分析
4.2 测试结果
第5章 用户使用说明
参考文献
附 录
第二篇:基于Linux网络编程课程设计
20##级通信工程
Linux网络编程课程设计报告
20##年6月29日
一、背景... 4
1.1 开发背景... 4
1.2 linux介绍... 4
二、实验目的... 6
三、实验步骤... 6
3.1交叉编译平台的安装... 6
四、设计原理... 7
4.1设计原理流程... 7
4.2程序流程分析... 7
五、网络套接字(socket)的概念... 9
5.1 Socket 介绍... 9
5.2 Socket 原理简介... 10
5.3 Socket 通信过程与程序开发... 11
六、系统实现... 16
6.1 Linux提供的有关Socket的系统调用... 16
6.2 监听连接... 18
6.3发送请求... 19
6.4主机接收请求,进行数据通信... 19
七、 运行效果... 19
一、背景
1.1 开发背景
在网络无所不在的今天,在Internet上,有ICQ、MSN、Gtalk、OICQ等网络聊天软件,极大程度上方便了处于在世界各地的友人之间的相互联系,也使世界好像一下子缩小了,不管你在哪里,只要你上了网,打开这些软件,就可以给你的朋友发送信息,不管对方是否也同时在线,只要知道他有号码。
Linux 操作系统作为一个开源的操作系统被越来越多的人所应用,它的好处在于操作系统源代码的公开化!只要是基于GNU公约的软件你都可以任意使用并修改它的源代码。但对很多习惯于Windows操作系统的人来说,Linux的操作不够人性化、交互界面不够美观,这给Linux操作系统的普及带来了很大的阻碍。因此制作一个Linux 操作系统下的拥有人性化界面的实时通讯工具,将给那些刚刚接触Linux操作系统的用户带来极大的方便,而且通过设计这样的一个应用程序还能更好的学习网络编程知识和掌握LINUX平台上应用程序设计开发的过程,将大学四年所学知识综合运用,以达到检验学习成果的目的。
1.2 linux介绍
Linux是一种针对PC计算机和工作站的操作系统,它具有像Windows和Mac那样的功能齐全的图形用户界面(GUI,Graphical User Interface)。Linus Torvald和其它的遍布世界各地的编程人员共同开发的。作为一种操作系统,它具有与Unix,Mac,Windows和Windows NT同样的功能。
提到Linux我们不能不提GNU和Unix。Richard M.Stallman建立的自由软件联盟出版了两种许可证,GNU通用公共许可证(GNU Gneral Public License,GPL)和GNU函数库通用公共许可证(GNU Library Gneral Public License,LGPL)。大部分GNU工程的软件和文档是以GNU通用公共许可证发行的,但是有一些库是以GNU函数库通用公共许可证发行的。按照GNU通用公共许可证的规定,Linux的源代码可以自由获取,这满足了我们学习该系统的强烈愿望。GPL充分体现了Stallman的思想:只要用户所做的修改是同等自由的,用户可以自由地使用、拷贝、查询、重用、修改甚至发布这个软件。通过这种方式,GPL保证了Linux(以及同一许可证下的大量其他软件)不仅现在自由可用,而且皮后经过任何修改这后都仍然可以自由使用。
Unix是由AT—T贝尔实验室的Ken Thompson和Dennis Ritchie于1969年在一台已经废弃了的PDP-7上开发的;最初它是一个用汇编语言写成的单用户操作系统。后来,他们又在PDP-11上用C语言重新编写(发明C语言的部分目的就在于此),把Unix做成为了一个文本处理系统,这使Unix在贝尔实验室得到广泛的应用。Unix的最初版本免费提供给许多知名的大学的计算机系使用。加州大学伯克利分校的计算机系就是其中的一名,并地Unix进行了修改增加了许多新的特点,这就是主为人知的BSC版本的Unix。与此同时,其它独立开发的Unix版本也开始萌生。Unix不断发展了,各种版本被应用到不同的计算机使用。而Linux最初是专门为基于Intel的个人计算机设计的。
(1)Linux的昨天
1991年,一名叫Linus Torvalds的芬兰大学生对Unix各种版本对于80386类的机器的脆弱支持十分不满。他决定要开发出一个全功能的、支持POSIX标准的、类Unix的操作系统内核,该系统吸收了BSD和System V 的优点,同进摒弃了它们的缺点。他独立把这个内核开发到0.02版,这个版本已经可以运行gcc、bash和很少的一些应用程序。后来,他又开始了在因特网上寻求广泛的帮助。1994年,Linux已经升级到1.0版本。它的源代码量也呈指数形式增长,实现了基本的TCP/IP功能,此时Linux已经拥有大约10万的用户。
(2) Linux的今天
作为一各服务器级的操作系统,Linux已经成熟了。现在的Linux内核由150多行代码组成,能作为Web服务器平台,也为越来越多的商业用户提供文件和打印服务。它既被当作邮件服务器的一种候选平台,也被当作一种强壮而安全的防火墙。
Linux的企业级特性,比如支持多处理器、支持大型文件系统、日志文件系统以及密集型计算和高可用性集群技术,也逐步成熟。
桌面上的Linux也在继续完善。KDE桌面提供的图形用户界面在易用性和可配置方面都能和微软的Windows相媲美。
(3) Linux的明天
Linux最强大的生命力在于其公开的开发过程。每个人都有可以自由获取内核源程序,每个人都有要不得以运载源程序加以修改,而后他人也可以自由获取你修改后的源程序。Linux这种独特的自由流畅的开发模型已被命名为bazaar(集市模型)。Bazaar开发模型通过重视实验,征集并充分利用早期的反馈,对巨大数量的脑力资源进行平衡配置,可以开发出更优秀的软件。本联盟就是想通过bazaar开发模型,在网上召集一些Linux的爱好者,开发出更优秀的操作系统或软件。
二、实验目的
2.1掌握ARM Linux交叉编译平台的搭建
2.2掌握Makefile文件的编写
2.3掌握编译工具arm-linux-gcc
2.4熟悉程序下载过程
三、实验步骤
3.1交叉编译平台的安装
(1) 考虑到现今Linux平台发展,交叉编译平台编统一为arm-linux-gcc-4.4.3
(2) 在windows系统下,建立一个共享目录,如:e:/linux_file
(3) 将光盘目录linux\中的arm-linux-gcc-4.4.3.tar.gz 复制到e:/linux_file
(4) 注意在进行虚拟机设置时使共享目录的有效,并添加共享目录e:/linux_file
(5) 进入linux操作系统,root目录下建立一个Armcode的子目录,将共享目录下的文件arm-linux-gcc-4.4.3.tar.gz复制到该目录
(6) 然后进入到该目录,执行解压命令:#cd /root/Armcode;
#tar xvzf arm-linux-gcc-4.4.3.tgz –C /; 注意:C 后面有个空格,并且C 是大写的,它是英文单词“Change”的第一个字母,在此是改变目录的意思。
(7) 执行该命令,将把arm-linux-gcc 安装到/opt/FriendlyARM/toolschain /4.4.3/bin 目录。
(8) 把编译器路径加入系统环境变量,运行命令:
#gedit /root/.bashrc
编辑/root/.bashrc 文件,在最后一行添加:
export PATH=$PATH: /opt/FriendlyARM/toolschain/4.4.3/bin
(9) 重新登录系统(不必重启机器,开始->logout 即可),使以上设置生效,在命令行输入:arm-linux-gcc –v,会出现如下信息,这说明交叉编译环境已经成功安装。
四、设计原理
4.1设计原理流程
网络编程,一定离不开套接口;那什么是套接口呢?在Linux下,所有的I/O操作都是通过读写文件描述符而产生的,文件描述符是一个和打开的文件相关联的整数,这个文件并不只包括真正存储在磁盘上的文件,还包括一个网络连接、一个命名管道、一个终端等,而套接口就是系统进程和文件描述符通信的一种方法。
服务器端:
(1) 创建一个socket,用函数socket();
(2) 绑定IP地址、端口等信息岛socket上,用函数bind();
(3) 设置应许的最大连接数,用函数listen();
(4) 接收客户端上来的链接,用函数accept();
(5) 接收数据,用recv();
(6) 关闭网络链接。
客户端:
(1) 创建一个socket,用函数socket();
(2) 设置要连接的对方的IP地址和端口等属性;
(3) 连接服务器,用函数connect();
(4) 发送数据,用函数send();
(5) 关闭网络连接。
4.2程序流程分析
(1)创建Socket
不管是客户端还是服务器端,都需要先创建一个socket。调用方式为:
int socket(int domain,int type,int protocol)
功能:创建一个新的套接字。
参数:domain: 指明所使用的协议族
type: 指明套接字的类型
protocol: 通常赋值“0”
返回值:成功时,返回一个整形socket描述符;失败时,返回-1。
设计过程具体调用为:
sock_fd = socket(AF_INET,SOCK_STREAM,0);
(2)服务器端使用的函数
1)int bind(int sockfd, struct sockaddr *my_addr, int addrlen)
sockfd:是由socket调用返回的文件描述符。
Addrlen:是sockaddr结构的长度。
my_addr:是一个指向sockaddr的指针。
由于我们主要使用Internet 所以sin_family 一般为AF_INET,sin_addr设置为INADDR_ANY表示可以和任何的主机通信,sin_port是要监听的端口号,sin_zero[8]是用来填充的。bind 将本地的端口同socket 返回的文件描述符捆绑在一起,成功是返回0。
2)int listen(int sockfd,int backlog)
sockfd:是bind后的文件描述符。
backlog:设置请求排队的最大长度,当有多个客户端程序和服务端相连时,使用这个表示可以介绍的排队长度。listen函数将bind的文件描述符变为监听套接字,返回的情况和bind一样。
3)int accept(int sockfd, struct sockaddr *addr,int *addrlen)
sockfd:是listen后的文件描述符。
addr,addrlen:是用来给客户端的程序填写的,服务器端只要传递指针就可以了。bind、listen和accept是服务器端用的函数,accept调用时服务器端的程序会一直阻塞到有一个客户程序发出了连接。accept成功时返回最后的服务器端的文件描述符,这个时候服务器端可以向该描述符写信息了,失败时返回-1。
4)int connect(int sockfd, struct sockaddr * serv_addr,int addrlen)
sockfd:socket返回的文件描述符。
serv_addr:储存了服务器端的连接信息,in_add是服务端的地址。
addrlen:serv_addr的长度。connect函数是客户端用来同服务端连接的,成功时返回0,sockfd是同服务端通讯的文件描述符,失败时返回-1。
(3)客户端使用的函数
1)connect( SOCKET s, const struct sockaddr FAR* name, int namelen);
s:标识一个未连接套接口的描述字。
name:欲进行连接的端口名。
namelen:名字长度。
若无错误发生,则connect()返回非0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。对阻塞套接口而言,若返回值为SOCKET_ERROR则应用程序调用WSAGetLsatError()。如果它指出错误代码为WSAEWOULDBLOCK,则您的应用程序可以:
a.用select(),通过检查套接口是否可写,来确定连接请求是否完成。
b.如果您的应用程序使用基于消息的WSAAsynSelect()来表示对连接事件的兴趣,则当连接操作完成后,您会收到一个FD_CONNECT消息。
2)send( SOCKET s, const char FAR* buf, int len, int flags);
s:一个用于标识已连接套接口的描述字。
buf:包含待发送数据的缓冲区。
len:缓冲区中数据的长度。
flags:调用执行方式。
五、网络套接字(socket)的概念
5.1 Socket 介绍
上世纪中后期, 在美国国防部高研署(DARPA)将TCP /IP 的软件提供给加利尼亚大学Berkeley 分校后, TCP /IP 很快被集成到Unix 中, 同时出现了许多成熟的TCP /IP 应用程序接口(API) 。这个API 称为Socket 接口( 套接口) 。Socket 在计算机中提供了一个通信端口, 可以通过这个端口与任何一个具有Socket 接口的计算机通信。应用程序在网络上传输,接收的信息都通过这个Socket 接口来实现。在应用开发中就像使用文件句柄一样, 可以对Socket 句柄进行读、写操作。今天, Socket 接口是TCP /IP 网络最为
通用的API,也是在Internet 上进行应用开发最为通用的API。Linux 操作系统具有良好的稳定性和出色的网络性能,因此被广泛应用于网络服务领域。而在Linux下开发高性能的网络通信程序,是充分发挥Linux 网络特性的一个关键因素。
5.2 Socket 原理简介
当用电话与他人通信时, 必须拿起话筒, 拨叫对方的电话号码, 然后等待对方的应答; 当双方进行通话的时候, 就建立了一个具有两个端点的通信线路,这两个端点是:本地的电话号码( 在本地位置) ;对方的电话号码( 在对方的位置处) 。双方的通信与通信的两个端点和他们之间的通信线路有关。Linux 中的套接口与电话非常相似。套接口代表通信线路中的端点, 两端点之间就是数据通信网络。套接口与电话的相似性还表现在另一方面。当给某人打电话时, 拨叫的是对方用户的电话号码。而套接口中的网络地址就相当于电话号码。通过在程序中指定远程套接口的地址, 就可以建立从本地套接口到
远端套接口的通信。TCP /IP 是计算机互连最常使用的网络通讯协议, TCP /IP 的核心部分由网络操作系统的内核实现,应用程序通过编程接口来访问TCP /IP。套接字( Socket) 是介于网络应用层和传输层之间的编程接口, 套接字接口提供了访问下层通信协议的大量系统调用和相应的数据结构。在Linux 中, 套接字接口是应用程序访问下层的网络协议的惟一方法。具体讲,套接字在用户级实现了两个应用程序之间的网络连接和数据交换, 所以Linux 中的套接字意味着网络上的连接。套接字在TCP /IP 网络模型中的地位如图4所示。
图4 socket在TCP/IP网络模型中的示意图
Socket 分为以下三种类型:
流式套接字( Stream Socket) : 是最常用的套接字类型, 文件传送协议( FTP) 即使用流式套接字。提供一个面向连接、可靠的数据传输服务, 数据无差错、无重复地发送, 且按发送顺序接收。内设流量控制, 避免数据流超限; 数据被看作是字节流, 无长度限制。数据报套接字(Datagram Socket) : TCP /IP 协议族中的UDP 协议使用此类接口, 它是无连接的服务,数据通过相互独立的报文进行传输, 提供了一个无连接服务。数据包以独立包形式被发送, 不提供无错保证, 数据可能丢失或重复, 并且接收顺序混乱。原始数据报套接字(Raw Socket) : 该接口允许对较低层协议, 如IP、ICMP 直接访问。常用于检验新的协议实现或访问。
5.3 Socket 通信过程与程序开发
(1) Socket 通信过程
基于TCP 可靠连接的客户与服务器连接进程流程图如图
客户与服务器的socket连接步骤流程
Socket 工作过程如下: 服务器首先启动, 通过调用Socket (), 建立一个Socket, 然后调用bind()将该Socket 和本地网络地址绑系在一起, 再调用listen()使Socket 做好侦听的准备, 并规定它的请求队列的长度,之后就调用accept()来接收连接。客户在建立Socket 后就可调用connect()和服务器建立连接。连接一旦建立,客户机和服务器之间就可以通过调用send()和recv()来发送和接收数据。最后, 待数据传送结束后, 双方调用close()关闭Socket。
(2)Socket 通信程序开发
下面详细说明Socket 通信程序的开发过程:
1) 步骤1: 建立套接口socket()
int sockfd = socket ( int domain, int type, intprotocol) , 其中domain 参数指定socket 协议族, 包括PF_LOACL 和PF_INET, PF_LOACL 表示使用本地套接口, PF_INET 表示使用Internet 套接口。Type 参数定义了套接口的类型, 包括SOCK_STREAM 和SOCK_DGRAM, SOCK_STREAM 指定为流套接口,SOCK_DGRAM指定为数据报套接口。protocol 通常赋值“0”, 意味套接口使用TCP /IP 协议。socket()调用返回一个整型socket 描述符, 可以在后面的调用使用它, 当其值为- 1 时, 说明有错误发生。
2) 步骤2: 绑定套接口bind()
当用socket()建立套接口后, 该套接口还是处于无名状态的, 无名套接口就象没有号码的电话一样,别人无法向发送信息( 在同一linux 内核下可实现无名状态下通信) 。为了像电话分配电话号码一样, 可以通过bind()为建立的套接口绑定一个名字———分配地址。这一步对客户端不是必需的。
int bind ( int sockfd, struct sockaddr _my_addr,
int addrlen) ;sockfd 是一个socket 描述符, my_addr 是一个指图计算机应用向包含有本机IP 地址及端口号等信息的sockaddr 类型的指针; addrlen 常被设置为sizeof ( structockaddr) , 如果函数调用成功, 就返回0, 否则就返回为- 1, 我们可以通过查看errno 的值来了解错误的原因。需要指出的是, 可以用下面的赋值实现自动获得本机IP 地址和随机获取一个没有被占用的端口号:my_addr.sin_port=0; /< 系统随机选择一个未被使用的端口号< /my_addr.sin_addr.s_addr=INADDR_ANY; /< 填入本机IP 地址< /通过将my_addr.sin_port 置为0, 函数会自动为选择一个未占用的端口来使用。同样, 通过将my_addr.sin_addr.s_addr 置为INADDR_ANY, 系统会自动填入本机IP 地址。bind()函数在成功被调用时返回0; 遇到错误时返回“- 1”并将errno 置为相应的错误号。另外要注意的是, 当调用函数时, 一般不要将端口号置为小于1024 的值, 因为1~1024 是保留端口号, 可以使用大于1024 中任何一个没有被占用的端口号。( 3) 步骤3: 请求连接connect()当客户端绑定地址后, 发送请求连接信号connect()来与远端服务器建立一个TCP 连接。connect()函数原型为:int connect ( int sockfd, struct sockaddr<serv_addr, int addrlen) ;sockfd 是目的服务器的socket 描述符; serv_addr是包含目的机IP 地址和端口号的指针, addrlen 为结构的大小。遇到错误时返回- 1, 并且errno 中包含相应的错误码。进行客户端程序设计无须调用bind(), 因为这种情况下只需知道目的机器的IP 地址, 而客户通过哪个端口与服务器建立连接并不需要关心, 内核会自动选择一个未被占用的端口供客户端来使用。( 4) 步骤4: 监听连接listen()在服务器端程序中, 当socket 与某一端口捆绑以后, 就需要监听该端口, 以便对到达的服务请求加以处理。
int listen( int sockfd, int backlog) ;sockfd 是Socket 系统调用返回的socket 描述符;backlog 指定在请求队列中允许的最大请求数, 进入的连接请求将在队列中等待accept()它们。backlog 对队列中等待服务的请求的数目进行了限制, 对于小型服务器, 队列长度应该为5 或是稍大一些的值, 而对于网站服务器, 我们就需要更大的值, 比如说16 或是更大。当listen 遇到错误时返回- 1, errno 被置为相应的错误码。
步骤5: 连接端口的服务请求
当某个客户端试图与服务器监听的端口连接时,该连接请求将排队等待服务器accept()它。通过调用accept()函数为其建立一个连接, accept()函数将返回一个新的socket 描述符, 来供这个新连接来使用。而服务器可以继续在以前的那个socket 上监听, 同时可以在新的socket 描述符上进行数据send ()( 发送) 和recv()( 接收) 操作。
int accept( int sockfd, void <addr, int <addrlen) ;
sockfd 是被监听的socket 描述符, addr 通常是一个指向sockaddr_in 变量的指针, 该变量用来存放提出连接请求服务的主机的信息( 某台主机从某个端口发出该请求) ; addrlen 通常为一个指向值为sizeof( struct sockaddr_in) 的整型指针变量。错误发生时返回一个- 1 并且设置相应的errno 值。accept()之前, 通常将addrlen 初始化为0。
步骤6: 数据传输send()和recv()
send()和recv()这两个函数是用于面向连接的socket 上进行数据传输。
send()函数原型为:
int send ( int sockfd, const void <msg, int len, intflags) ;
sockfd 是想用来传输数据的socket 描述符, msg是一个指向要发送数据( 可以是字符型、整型、浮点型等) 的指针。len 是以字节为单位的数据的长度。flags
一般情况下置为0。
send()函数返回实际上发送出的字节数, 可能会少于希望发送的数据。所以需要对send()的返回值进行测量。当send()返回值与len 不匹配时, 应该对这种情况进行处理。
recv()函数原型为:
int recv ( int sockfd, void <buf, int len, unsignedint flags) ;
sockfd 是接收数据的socket 描述符; buf 是存放接收数据的缓冲区; len 是缓冲的长度。flags 也被置为0。recv()返回实际上接收的字节数, 或当出现错误时,返回- 1 并置相应的errno 值。
步骤7: 关闭连接close()
当所有的数据操作结束以后, 可以调用close()函数来释放该socket, 从而停止在该socket 上的任何数据操作。
5.4Socket
简要步骤如下:
(1)建立一个Socket。
(2)按要求配置socket,将socket连接到远程主机或给socket指定以各本地协议端口。
(3)按要求通过socket发送和接受数据。
(4)关闭此socket。
这是通过Socket实现点对点需要掌握的4个编程要点。
5.5多线程的概念
上述点对点通信的实现知识完成了主机进程与服务器进程之间的连接,建立连接的进程之间是一对一的联系,即主机的一个进程与服务器的一个进程之间建立的连接。而每个进程进行通信的环节都包括了发送信息和接口信息两个任务,这两个任务通过一个端口地址发送和接收。
对于多个并发的任务需要创建多个线程或线程去实现。使用一个进程去完成发送信息是没有问题的,因为发送总是主动的;而使用同一个进程再去完成接受信息去不一定会成功,因为接受信息是被动的,所以当没有信息可以接收时,该进程就会被阻塞,从而导致发送任务也一起被阻塞。同一个端口的发送和接收是两个并发任务,应该由两个不同的任务去分别完成信息的发送和接收。这样,当接收信息任务因没有信息而被阻塞时,不至于影响发送任务的执行。
那么,发送和接收两个任务是使用两个进程还是两个进程去完成呢?
在网络通信中,端口地址是以进程为单位进程分配的,而一个进程与外界的消息发送与接收必须通过分配给它的同一个端口进行。因此,不能通过创建进程方式来解决上诉问题,因为两个进程会分别对应两个不同的端口,而发送和接收必须使用同一端口。线程不是资源分配的单位,所以如果使用两个线程不会对线程分配新的端口。因此,本实验需要使用两个线程去分别完成发送和接收信息的任务,这两个线程共享其进程拥有的统一个端口地址。由于创建进程的进程本身会作为一个线程来调度,所以只需要再创建一个线程专门负责接收信息就可以了。
因此,对于从每个客户端发来的请求,服务器端都要创建相应的线程去接收并处理;同理,对于客户端而言,也要创建一个线程去读取服务器端发来的信息。
六、系统实现
6.1 Linux提供的有关Socket的系统调用
(1)Socket()
作用:socket函数为客户机或服务器创建一个sokcet
格式:
int socket(int family,int type,int protocol);
参数说明:
Family:表示地址族,可以去AF_UNLX和AF_INT。
其中,AF_UNLX只能够用于单一的UNIX系统进程间通信;AF_INT是针对Internet的,因而可以允许在远程主机之间通信,实验中使用AF_INT。
Type:网络程序所采用的通信协议,可以取SOCK_STREAM或SOCK_DGRAM。其中,SOCK_STREAM表明使用的是TCP协议,这样提供按顺序的、可靠的、双向、面向连接的比特流;SOCKE_DGRAM表明使用的是UDP协议,这样只会提供定长、不可靠、无连接的通信。
(2)bind( )
格式:
int bind(int sockfd,struct sockaddr *addr,int addrlen);
参数说明:
Sockfd:socket的文件描述符号。
Sockaddr:表示名字所用的一个数据结构,用来保存地址(包括IP地址和端口)
Addrlen:设置结构大小长度。
(3)listen()
格式:
int listen(int sockfd, int backlog);
作用:监听连接信号,和accepted函数合同。
参数说明:
Sockfd:表示socket调用返回的文件描述符。
Backlog:表示接入队列允许的连接数目,大多数系统允许20个,也可以子定义5~10个。
(4)accept()
格式:
Int accept (int sockfd, void *addr, int *addrlen);
作用:与listen函数合用,监听信息、接收客户端请求。
参数说明:
Sockfd:表示socket的文件描述符。
Addr:表示指向局部的数据结构struct sockaddr-in的指针。
Addrlen:表示地址的长度。
(5)connect()
格式:
int connect( int sockfd , struct sockaddr *serv_addr , int addrlen);
作用:在面向连接的系统中客户及连接服务器时使用,connect必须在bind后使用。
参数作用:
Sockfd:表示socket的文件描述符。
Serv-addr:表示村访目的端口和ip地址(套接字)的数据结构。
(6)send() 和 recv()
格式1:
Int send (int sockfd, const vod *msg,int len, int flags);
功能:发送信息。
格式2:
Int recv (int sockfd , void *buf,int len, usigned int flags);
作用:用于流式socket、数据报socket内部之间的通信。
(7)close( ) 和 shutdown ( )
格式:
Close( int sockfd)或
Int shutdown(int sockfd , int how);
参数说明:
How的值为下面一种:
0----不允许继续接收;
1----不允许继续发送;
2---不允许继续发送和接收。
(8)有关线程的系统调用函数pthread_create()、pthread_join()。
6.2 监听连接
利用socket、bind、listen建立连接,步骤是:
1、先用socket函数初始化socket,创建新的sockfd。
Sockfd = socket(AF_INT,SOCK_STREAM,0)
2、此步骤涉及到IP地址及其处理过程。
参数说明:
inet_addr 函数 INADDR_ANY
该函数把由小数点分开的十进制IP地址转为unsinged long 类型,而在实验中所使用的为INADDR_ANY,使用利用自已的IP地址自动填充。
(1)利用bind函数绑定端口和IP地址。
My_addr.sin_family=AF_INET; /*将地址族类型设定好 */
My_addr.sin_port=htons(MYPORT; /* 将端口给其赋值*/
My_addr.sin_addr.s_addr=INADDR_ANY; /*用连接地址自动填充ip*/
Bind(sockfd,(stuct sockaddr*)&my_addr,sizeof(stuct sockaddr));
/*sockfd 是分配的socket名字,my-addr则便是分配好的端口与IP,用bind绑定*/
(2)利用listen监听请求
6.3发送请求
(1)利用gethostbyname获取主机信息。
(2)初始化socket端口。
(3)利用connect函数将自己的IP地址等信息发送到主机,等待主机调用accept函数来接受请求。
6.4主机接收请求,进行数据通信
(1)主机利用accept接收请求。
(2)创建子进程,显示欢迎信息;
(3)接收返回信息,显示连接成功,并推出连接;
(4)关闭客户端口socket;
(5)关闭服务端socket,结束子线程。
七、 运行效果
程序测试环境:
linux、unix、debian等操作系统。
测试软件:
putty、vmware虚拟机
(1) 在编写完TCP服务端程序server.c后,用 gcc –lpthread –o server.c server 生成程序server。
(2) 在编写完TCP客户端程序client.c后,用gcc –lpthread –o client.c client 生成程序client
(3) 在主机上打开一窗口,运行server。
(4) 再打开另一个窗口或者在另一个主机上打开一个窗口,运行client,输入服务器的IP地址,并检查器结果的正确性。
输入:
【主】# ./server
【从】# ./client 127.0.0.1 (127.0.0.1为本机的ip地址)
输出:
【主】#server:got connection from 127.0.0.1
(5) 客户端、服务器端窗口之间以及交错发送信息的方式相互发送和接收信息。
1) 客户端、服务器端窗皆通过键盘输入消息内容平回车,以发送消息给对方;
2) 消息中若使用空格,则作为本条消息结束及下一条消息的开始;
3) 输入exit则推出运行。
开始运行后,服务器端窗口的执行顺序为:
1) 键入“Hello,world!”发送给客户端
2) 接收客户端发来的两个消息;
3) 键入“OK!”发送个客户端;
4) 输入exit结束。
服务器端运行结果如实验图所示。
服务器端窗口
开始运行后,客户端窗口的执行顺序为:
1) 接收服务器端发来的消息“Hello,world!”;
2) 发送消息“hello!“和”Good!”给服务器端;
3) 接收服务器发来的消息“OK!”;
4) 键入exit结束。
客户端运行结果如实验图4-2所示。
客户端窗口
上述运行结果表明,客户端与服务器端之间传递的消息已被对方成功接收。
附录:
(1)编写服务器端程序 tcp_server.c
#include "common.h"
void sig(int signum)
{
psignal(signum,"Catch signal:");
signal(signum,sig);
}
int main (int argc, char *argv[])
{
int sock_fd,conn_fd;
struct sockaddr_in server_addr,client_addr;
socklen_t addrlen = ADDR_SIZE;
int wc = -1,rc = -1;
char buffer_r[BUFFER_SIZE],buffer_w[BUFFER_SIZE];
int i = 1;
pid_t pt;
int val;
fd_set readfds;
struct timespec t_val;
int max_fd;
int ret;
sigset_t sigset,newset;
sigemptyset(&sigset);
sigemptyset(&newset);
sigaddset(&sigset,SIGINT);
sigaddset(&sigset,SIGALRM);
sock_fd = socket(AF_INET,SOCK_STREAM,0);
if(sock_fd == -1)
Err_sys("Server socket:")
bzero(&server_addr,ADDR_SIZE);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(Server_port);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
setsockopt(sock_fd,SOL_SOCKET,SO_REUSEADDR,(void *)&i,sizeof(i));
if(bind(sock_fd,(struct sockaddr *)&server_addr,addrlen) == -1)
Err_sys("Server bind:")
if(listen(sock_fd,2) == -1)
Err_sys("Server listen:")
while(RUNNING)
{
conn_fd = accept(sock_fd,(struct sockaddr *)&client_addr,&addrlen);
sleep(3);
printf("accept success!\n");
if(conn_fd == -1)
fprintf(stderr,"Server accept:%s",strerror(errno));
printf("Connect client [ip]:%s [port]:%d\n",inet_ntoa(client_addr.sin_addr), \
ntohs(client_addr.sin_port));
if((pt = fork()) == -1)
Err_sys("Server fork:")
else if(pt > 0)
close(conn_fd);
else
{
close(sock_fd);
signal(SIGALRM,sig);
printf("signal success\n");
sigprocmask(SIG_BLOCK,&sigset,NULL);
raise(SIGALRM);
sleep(10);
while(RUNNING)
{
FD_ZERO(&readfds);
FD_SET(0,&readfds);
FD_SET(conn_fd,&readfds);
t_val.tv_sec = 7;
t_val.tv_nsec = 0;
max_fd = conn_fd;
if((ret = pselect(max_fd + 1,&readfds,NULL,NULL,&t_val,&sigset)) == -1)
Err_sys("Server select:")
else if(ret == 0)
{
Err_sys ("Select timeout\n");
continue;
}
else
{
if(FD_ISSET(conn_fd,&readfds))
{
memset(buffer_r,0,BUFFER_SIZE);
rc = recv(conn_fd,buffer_r,BUFFER_SIZE,0);
if(rc <= 0)
{
fprintf(stderr,"Server recv:%s\n",strerror(errno));
FD_CLR(conn_fd,&readfds);
close(conn_fd);
}
printf("[Server recv]:%s\n",buffer_r);
} //read set
if(FD_ISSET(0,&readfds))
{
memset(buffer_w,0,BUFFER_SIZE);
printf("[Server send]:");
fflush(stdout);
fgets(buffer_w,BUFFER_SIZE,stdin);
wc = send(conn_fd,buffer_w,BUFFER_SIZE,0);
if(wc <= 0)
fprintf(stderr,"Server send:%s\n",strerror(errno));
} //write set
}
} //while rw
close(conn_fd);
exit(EXIT_SUCCESS);
}//else child
} //while accept
close(conn_fd);
close(sock_fd);
return 0;
}
(2)编写客户端程序tcp_client.c
#include "common.h"
int main (int argc, char *argv[])
{
int sock_fd;
printf("Sftp protocol test! \n");
struct sockaddr_in server_addr,client_addr;
socklen_t addrlen = ADDR_SIZE;
int wc = -1,rc = -1;
char buffer_r[BUFFER_SIZE],buffer_w[BUFFER_SIZE];
int con_times = 0;
int i = 1;
struct hostent *host;
fd_set readfds;
struct timeval t_val;
int max_fd;
int ret;
if(argc != 2)
{
fprintf(stderr,"Usage:%s ip\n",argv[0]);
exit(EXIT_FAILURE);
}
sock_fd = socket(AF_INET,SOCK_STREAM,0);
if(sock_fd == -1)
Err_sys("Client socket:")
host = gethostbyname(argv[1]);
bzero(&server_addr,ADDR_SIZE);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(Server_port);
server_addr.sin_addr = *((struct in_addr *)host->h_addr);
while(connect(sock_fd,(struct sockaddr *)&server_addr,addrlen) == -1)
{
if(con_times < 3)
{
printf("Client connecting ...........\n");
sleep(3);
}
else
{
printf("Client connected failfully \n");
exit(EXIT_FAILURE);
}
con_times++;
} //while
while(RUNNING)
{
FD_ZERO(&readfds);
FD_SET(0,&readfds);
FD_SET(sock_fd,&readfds);
t_val.tv_sec = 1;
t_val.tv_usec = 0;
max_fd = sock_fd;
fflush(stdin);
if((ret = select(max_fd + 1,&readfds,NULL,NULL,&t_val)) == -1)
Err_sys("Client select:")
else if(ret == 0)
{
Err_sys ("Select timeout\n");
continue;
}
else
{
printf("1111111111111111\n");
if(FD_ISSET(0,&readfds))
{
memset(buffer_w,0,BUFFER_SIZE);
printf("[Client send]:");
fflush(stdout);
fgets(buffer_w,BUFFER_SIZE,stdin);
wc = send(sock_fd,buffer_w,BUFFER_SIZE,0);
if(wc <= 0)
Err_sys("Client send:")
} //read set
if(FD_ISSET(sock_fd,&readfds))
{
memset(buffer_r,0,BUFFER_SIZE);
fflush(stdout);
rc = recv(sock_fd,buffer_r,BUFFER_SIZE,0);
if(rc <= 0)
Err_sys("Client recv:")
printf("[Client recv]:%s\n",buffer_r);
} //write set
}//else if
} //while
close(sock_fd);
return 0;
}
(3)编写公共头文件common.h
#ifndef __NETWORK__H__
#define __NETWORK__H__
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <error.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/select.h>
#include <sys/time.h>
#pragma once
#pragma pack(4)
#define BUFFER_SIZE 1024
#define Server_port 8888
#define BACKLOG 5
#define ADDR_SIZE sizeof(struct sockaddr_in)
#define RUNNING 1
#define Err_sys(info) \
{ \
fprintf(stderr,"%s:%s\n",info,strerror(errno)); \
exit(EXIT_FAILURE); \
}
#endif
(4)编写Makefile文件
all:
gcc -g -o tcp_server tcp_server.c
gcc -g -o tcp_client tcp_client.c
clean:
rm tcp_server tcp_client
(5)make