socket编程原理
1、问题的引入
1) 普通的I/O操作过程:
UNIX系统的I/O命令集,是从Maltics和早期系统中的命令演变出来的,其模式为打开一读/写一关闭(open-write-read-close)。在一个用户进程进行I/O操作时,它首先调用“打开”获得对指定文件或设备的使用权,并返回称为文件描述符的整型数,以描述用户在打开的文件或设备上进行I/O操作的进程。然后这个用户进程多次调用“读/写”以传输数据。当所有的传输操作完成后,用户进程关闭调用,通知操作系统已经完成了对某对象的使用。
2) TCP/IP协议被集成到UNIX内核中
TCP/IP协议被集成到UNIX内核中时,相当于在UNIX系统引入了一种新型的I/O操作。UNIX用户进程与网络协议的交互作用比用户进程与传统的I/O设备相互作用复杂得多。首先,进行网络操作的两个进程在不同机器上,如何建立它们之间的联系?其次,网络协议存在多种,如何建立一种通用机制以支持多种协议?这些都是网络应用编程界面所要解决的问题。
3) 需要一种通用的网络编程接口: 独立于具体协议和通用的网络编程
在UNIX系统中,网络应用编程界面有两类:UNIX BSD的套接字(socket)和UNIX System V的TLI。由于Sun公司采用了支持TCP/IP的UNIX BSD操作系统,使TCP/IP的应用有更大的发展,其网络应用编程界面──套接字(socket)在网络软件中被广泛应用,至今已引进微机操作系统DOS和Windows系统中,成为开发网络应用软件的强有力工具,本章将要详细讨论这个问题。
开始使用套接字编程之前,首先必须建立以下概念。
2.1 网间进程通信
进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作系统为进程通信提供了相应设施,
如UNIX BSD有:管道(pipe)、命名管道(named pipe)软中断信号(signal)
UNIX system V有:消息(message)、共享存储区(shared memory)和信号量(semaphore)等.
他们都仅限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。为此,首先要解决的是网间进程标识问题。同一主机上,不同进程可用进程号(process ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋于某进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。 其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间进程通信还要解决多重协议的识别问题。 为了解决上述问题,TCP/IP协议引入了下列几个概念。
1)端口
网络中可以被命名和寻址的通信端口,是操作系统可分配的一种资源。
按照OSI七层协议的描述,传输层与网络层在功能上的最大区别是传输层提供进程通信能力。从这个意义上讲,网络通信的最终地址就不仅仅是主机地址了,还包括可以描述进程的某种标识符。为此,TCP/IP协议提出了协议端口(protocol port,简称端口)的概念,用于标识通信的进程。
端口是一种抽象的软件结构(包括一些数据结构和I/O缓冲区)。应用程序(即进程)通过系统调用与某端口建立连接(binding)后,传输层传给该端口的数据都被相应进程所接收,相应进程发给传输层的数据都通过该端口输出。在TCP/IP协议的实现中,对端口的操作类似于一般的I/O操作,进程获取一个端口,相当于获取本地唯一的I/O文件,可以用一般的读写原语访问之。 类似于文件描述符,每个端口都拥有一个叫端口号(port number)的整数型标识符,用于区别不同端口。
由于TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立,如TCP有一个255号端口,UDP也可以有一个255号端口,二者并不冲突。
端口号的分配是一个重要问题。有两种基本分配方式:第一种叫全局分配,这是一种集中控制方式,由一个公认的中央机构根据用户需要进行统一分配,并将结果公布于众。第二种是本地分配,又称动态连接,即进程需要访问传输层服务时,向本地操作系统提出申请,操作系统返回一个本地唯一的端口号,进程再通过合适的系统调用将自己与该端口号联系起来(绑扎)。TCP/IP端口号的分配中综合了上述两种方式。TCP/IP将端口号分为两部分,少量的作为保留端口,以全局方式分配给服务进程。因此,每一个标准服务器都拥有一个全局公认的端口(即周知口,well-known port),即使在不同的机器上,其端口号也相同。剩余的为自由端口,以本地方式进行分配。TCP和UDP均规定,小于256的端口号才能作保留端口。
2)地址
网络通信中通信的两个进程分别在不同的机器上。在互连网络中,两台机器可能位于不同的网络,这些网络通过网络互连设备(网关,网桥,路由器等)连接。因此需要三级寻址:
1. 某一主机可与多个网络相连,必须指定一特定网络地址;
2. 网络上每一台主机应有其唯一的地址;
3. 每一主机上的每一进程应有在该主机上的唯一标识符。
通常主机地址由网络ID和主机ID组成,在TCP/IP协议中用32位整数值表示;TCP和UDP均使用16位端口号标识用户进程。
3)网络字节顺序
不同的计算机存放多字节值的顺序不同,有的机器在起始地址存放低位字节(小端序,低价先存),有的存高位字节(大端序,高价先存)。为保证数据的正确性,在网络协议中须指定网络字节顺序。TCP/IP协议使用16位整数和32位整数的高价先存格式,它们均含在协议头文件中。 详解http://blog.csdn.net/hguisu/article/details/7449955#t1
4)连接
两个进程间的通信链路称为连接。连接在内部表现为一些缓冲区和一组协议机制,在外部表现出比无连接高的可靠性。
5)半相关
综上所述,网络中用一个三元组可以在全局唯一标志一个进程:
(协议,本地地址,本地端口号) 这样一个三元组,叫做一个半相关(half-association),它指定连接的每半部分。
6)全相关
一个完整的网间进程通信需要由两个进程组成,并且只能使用同一种高层协议。也就是说,不可能通信的一端用TCP协议,而另一端用UDP协议。因此一个完整的网间通信需要一个五元组来标识:
(协议,本地地址,本地端口号,远地地址,远地端口号) 这样一个五元组,叫做一个相关(association),即两个协议相同的半相关才能组合成一个合适的相关,或完全指定组成一连接。
2.2 服务方式
在网络分层结构中,各层之间是严格单向依赖的,各层次的分工和协作集中体现在不同层之间的界面上。“服务”是描述不同层之间关系的抽象概念,即网络中各层向紧邻上层提供的一组操作。下层是服务提供者,上层是请求服务的用户。服务的表现形式是原语(primitive),如系统调用或库函数。系统调用是操作系统内核向网络应用程序或高层协议提供的服务原语。网络中的n层总要向n+1层提供比n-1层更完备的服务,否则n层就没有存在的价值。 在OSI的术语中,网络层及其以下各层又称为通信子网,只提供点到点通信,没有程序或进程的概念。而传输层实现的是“端到端”通信,引进网间进程通信概念,同时也要解决差错控制,流量控制,数据排序(报文排序),连接管理等问题,为此提供不同的服务方式:
1)面向连接(虚电路)或无连接
面向连接服务(TCP协议)
:是电话系统服务模式的抽象,即每一次完整的数据传输都要经过建立连接,使用连接,终止连
接的过程。在数据传输过程中,各数据分组不携带目的地址,而使用连接号(connect ID)。本质上,连接
是一个管道,收发数据不但顺序一致,而且内容相同。TCP协议提供面向连接的虚电路。
无连接服务(UDP协议):是邮政系统服务的抽象,每个分组都携带完整的目的地址,各分组在系统中独立传送。无连接服务不能保证分组的先后顺序,不进行分组出错的恢复与重传,不保证传输的可靠性。UDP协议提供无连接的数据报服务。
下面给出这两种服务的类型及应用中的例子:
![](https://img-my.csdn.net/uploads/201204/10/1334025912_2353.jpg)
2)顺序
在网络传输中,两个连续报文在端-端通信中可能经过不同路径,这样到达目的地时的顺序可能会与发送时不同。“顺序”是指接收数据顺序与发送数据顺序相同。TCP协议提供这项服务。
3)差错控制
保证应用程序接收的数据无差错的一种机制。检查差错的方法一般是采用检验“检查和(Checksum)”的方法。而保证传送无差错的方法是双方采用确认应答技术。TCP协议提供这项服务。
4)流控制
在数据传输过程中控制数据传输速率的一种机制,以保证数据不被丢失。TCP协议提供这项服务。
5)字节流
字节流方式指的是仅把传输中的报文看作是一个字节序列,不提供数据流的任何边界。TCP协议提供字节流服务。
6)报文
接收方要保存发送方的报文边界。UDP协议提供报文服务。
7)全双工/半双工
端-端间数据同时以两个方向/一个方向传送。
8)缓存/带外数据
在字节流服务中,由于没有报文边界,用户进程在某一时刻可以读或写任意数量的字节。为保证传输正确或采用有流控制的协议时,都要进行缓存。但对某些特殊的需求,如交互式应用程序,又会要求取消这种缓存。 在数据传送过程中,希望不通过常规传输方式传送给用户以便及时处理的某一类信息,如UNIX系统的中断键(Delete或Control-c)、终端流控制符(Control-s和Control-q),称为带外数据。逻辑上看,好象用户进程使用了一个独立的通道传输这些数据。该通道与每对连接的流相联系。由于Berkeley Software Distribution中对带外数据的实现与RFC 1122中规定的Host Agreement不一致,为了将互操作中的问题减到最小,应用程序编写者除非与现有服务互操作时要求带外数据外,最好不使用它。
2.3 客户/服务器模式
在TCP/IP网络应用中,通信的两个进程间相互作用的主要模式是客户/服务器模式(Client/Server model),即客户向服务器发出服务请求,服务器接收到请求后,提供相应的服务。客户/服务器模式的建立基于以下两点:首先,建立网络的起因是网络中软硬件资源、运算能力和信息不均等,需要共享,从而造就拥有众多资源的主机提供服务,资源较少的客户请求服务这一非对等作用。其次,网间进程通信完全是异步的,相互通信的进程间既不存在父子关系,又不共享内存缓冲区,因此需要一种机制为希望通信的进程间建立联系,为二者的数据交换提供同步,这就是基于不同的客户/服务器模式的TCP/IP。 客户/服务器模式在工作过程中采取的是主动请求方式:
服务器方:
首先服务器方要先启动,并根据请求提供相应服务:
1. 打开一通信通道并告知本地主机,它愿意在某一公认地址上(周知口,如FTP为21)接收客户请求;
2. 等待客户请求到达该端口;
3. 接收到重复服务请求,处理该请求并发送应答信号。接收到并发服务请求,要激活一新进程来处理这个客户请求(如UNIX系统中用fork、exec)。新进程处理此客户请求,并不需要对其它请求作出应答。服务完成后,关闭此新进程与客户的通信链路,并终止。
4. 返回第二步,等待另一客户请求。
5. 关闭服务器
客户方:
1. 打开一通信通道,并连接到服务器所在主机的特定端口;
2. 向服务器发服务请求报文,等待并接收应答;继续提出请求......
3. 请求结束后关闭通信通道并终止。
从上面所描述过程可知:
1. 客户与服务器进程的作用是非对称的,因此编码不同。
2. 服务进程一般是先于客户请求而启动的。只要系统运行,该服务进程一直存在,直到正常或强迫终止。
2.4 套接字类型
TCP/IP的socket提供下列三种类型套接字。
流式套接字(SOCK_STREAM):
提供了一个面向连接、可靠的数据传输服务,数据无差错、无重复地发送,且按发送顺序接收。内设流量控
制,避免数据流超限;数据被看作是字节流,无长度限制。文件传送协议(FTP)即使用流式套接字。
数据报式套接字(SOCK_DGRAM):
提供了一个无连接服务(UDP)。数据包以独立包形式被发送,不提供无错保证,
数据可能丢失或重复,并且接收顺序混乱。网络文件系统(NFS)使用数据报式套接字。
原始式套接字(SOCK_RAW) :
该接口允许对较低层协议,如IP、ICMP直接访问。常用于检验新的协议实现或访问现有服务中配置的新设备。
2.4 典型套接字调用过程举例
如前所述,TCP/IP协议的应用一般采用客户/服务器模式,因此在实际应用中,必须有客户和服务器两个进
程,并且首先启动服务器,其系统调用时序图如下。
面向连接的协议(如TCP)的套接字系统调用如图2.1所示:
![](https://img-my.csdn.net/uploads/201204/10/1334028481_7031.jpg)
服务器必须首先启动,直到它执行完accept()调用,进入等待状态后,方能接收客户请求。假如客户在此前启动,则connect()将返回出错代码,连接不成功。
无连接协议(UDP)的套接字调用如图2.2所示:
![](https://img-my.csdn.net/uploads/201204/10/1334028513_1240.jpg)
无连接服务器也必须先启动,否则客户请求传不到服务进程。无连接客户不调用connect()。因此在数据发送之前,客户与服务器之间尚未建立完全相关,但各自通过socket()和bind()建立了半相关。发送数据时,发送方除指定本地套接字号外,还需指定接收方套接字号,从而在数据收发过程中动态地建立了全相关。
实例
本实例使用面向连接协议的客户/服务器模式,其流程如图2.3所示:
![](https://img-my.csdn.net/uploads/201204/10/1334028527_2527.jpg)
服务器方程序:
-
- #include <winsock.h>
- #include <stdio.h>
- #define TRUE 1
-
-
-
-
- main( )
- {
- int sock, length;
- struct sockaddr_in server;
- struct sockaddr tcpaddr;
- int msgsock;
- char buf[1024];
- int rval, len;
-
-
- sock = socket(AF_INET, SOCK_STREAM, 0);
- if (sock < 0) {
- perror(“opening stream socket”);
- exit(1);
- }
-
-
- server.sin_family = AF_INET;
- server.sin_port = INADDR_ANY;
- if (bind(sock, (struct sockaddr *)&server, sizeof(server)) < 0) {
- perror(“binding stream socket”);
- exit(1);
- }
-
-
- length = sizeof(server);
- if (getsockname(sock, (struct sockaddr *)&server, &length) < 0) {
- perror(“getting socket name”);
- exit(1);
- }
- printf(“socket port #%d/n”, ntohs(server.sin_port));
-
-
- listen(sock, 5);
- len = sizeof(struct sockaddr);
- do {
- msgsock = accept(sock, (struct sockaddr *)&tcpaddr, (int *)&len);
- if (msgsock == -1)
- perror(“accept”);
- else do{
- memset(buf, 0, sizeof(buf));
- if ((rval = recv(msgsock, buf, 1024)) < 0)
- perror(“reading stream message”);
- if (rval == 0)
- printf(“ending connection /n”);
- else
- printf(“-->;%s/n”, buf);
- }while (rval != 0);
- closesocket(msgsock);
- } while (TRUE);
-
-
-
- exit(0);
- }
客户方程序:
-
- #include <winsock.h>
- #include <stdio.h>
- #define DATA “half a league, half a league ...”
-
-
-
- main(argc, argv)
- int argc;
- char *argv[ ];
- {
- int sock;
- struct sockaddr_in server;
- struct hostent *hp, *gethostbyname( );
- char buf[1024];
-
-
- sock = socket(AF_INET, SOCK_STREAM, 0);
- if (sock < 0) {
- perror(“opening stream socket”);
- exit(1);
- }
-
-
- server.sin_family = AF_INET;
- hp = gethostbyname(argv[1]);
- if (hp == 0) {
- fprintf(stderr, “%s: unknown host /n”, argv[1]);
- exit(2);
- }
- memcpy((char*)&server.sin_addr, (char*)hp->;h_addr, hp->;h_length);
- sever.sin_port = htons(atoi(argv[2]));
-
- if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) {
- perror(“connecting stream socket”);
- exit(3);
- }
-
- if (send(sock, DATA, sizeof(DATA)) < 0)
- perror(“sending on stream socket”);
- closesocket(sock);
- exit(0);
- }
2.5 一个通用的实例程序
在上一节中,我们介绍了一个简单的socket程序实例。从这个例子我们可以看出,使用socket编程几乎有一
个模式,即所有的程序几乎毫无例外地按相同的顺序调用相同的函数。因此我们可以设想,设计一个中间层
,它向上提供几个简单的函数,程序只要调用这几个函数就可以实现普通的网络数据传输,程序设计者不必
太多地关心socket程序设计的细节。
本节我们将介绍一个通用的网络程序接口,它向上层提供几个简单的函数,程序设计者只要使用这几个函数
就可以完成绝大多数网络数据传输。这些函数将socket编程和上层隔离开来,它使用面向连接的流式套接字
,采用非阻塞的工作机制,程序只要调用这些函数查询网络消息并作出相应的响应即可。这些函数包括:
l InitSocketsStruct:初始化socket结构,获取服务端口号。客户程序使用。
l InitPassiveSock:初始化socket结构,获取服务端口号,建立主套接字。服务器程序使用。
l CloseMainSock:关闭主套接字。服务器程序使用。
l CreateConnection:建立连接。客户程序使用。
l AcceptConnection:接收连接。服务器程序使用。
l CloseConnection:关闭连接。
l QuerySocketsMsg:查询套接字消息。
l SendPacket:发送数据。
l RecvPacket:接收数据。
2.5.1 头文件
/* File Name: tcpsock.h */
/* 头文件包括socket程序经常用到的系统头文件(本例中给出的是SCO Unix下的头文件,其它版本的Unix的头文件可能略有不同),并定义了我们自己的两个数据结构及其实例变量,以及我们提供的函数说明。*/
-
-
-
- #include <stdio.h>
- #include <string.h>
- #include <time.h>
- #include <sys/tape.h>
- #include <sys/signal.h>
- #include <sys/errno.h>
- #include <sys/types.h>
- #include <sys/ioctl.h>
- #include <sys/select.h>
- #include <sys/socket.h>
- #include <sys/stat.h>
- #include <sys/netinet/in.h>
- #include <sys/netinet/tcp.h>
- #include <arpa/inet.h>
- #include <netdb.h>
-
- typedef struct SocketsMsg{
- int AcceptNum;
- int ReadNum;
- int ReadQueue[32];
- int WriteNum;
- int WriteQueue[32];
- int ExceptNum;
- int ExceptQueue[32];
- } SocketsMsg;
- typedef struct Sockets {
- int DaemonSock;
- int SockNum;
- int Sockets[64];
- fd_set readfds, writefds, exceptfds;
-
- } Sockets;
-
- Sockets Mysock;
- SocketsMsg SockMsg;
-
- int InitSocketsStruct(char * servicename) ;
- int InitPassiveSock(char * servicename) ;
- void CloseMainSock();
- int CreateConnection(struct in_addr *sin_addr);
- int AcceptConnection(struct in_addr *IPaddr);
- int CloseConnection(int Sockno);
- int QuerySocketsMsg();
- int SendPacket(int Sockno, void *buf, int len);
- int RecvPacket(int Sockno, void *buf, int size);
2.5.2 函数源文件
-
-
- #include "tcpsock.h"
-
- int InitSocketsStruct(char * servicename)
-
-
-
- {
- struct servent *servrec;
- struct sockaddr_in serv_addr;
-
- if ((servrec = getservbyname(servicename, "tcp")) == NULL) {
- return(-1);
- }
- bzero((char *)&Mysock, sizeof(Sockets));
- Mysock.Port = servrec->s_port;
- return(1);
- }
-
- int InitPassiveSock(char * servicename)
-
-
-
- {
- int mainsock, flag=1;
- struct servent *servrec;
- struct sockaddr_in serv_addr;
-
- if ((servrec = getservbyname(servicename, "tcp")) == NULL) {
- return(-1);
- }
- bzero((char *)&Mysock, sizeof(Sockets));
- Mysock.Port = servrec->s_port;
-
- if((mainsock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
- return(-2);
- }bzero((char *)&serv_addr, sizeof(serv_addr));
- serv_addr.sin_family = AF_INET;
- serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
- serv_addr.sin_port = servrec->s_port;
- if (bind(mainsock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
- close(mainsock);
- return(-3);
- }
-
- if (listen(mainsock, 5) == -1) {
- close(mainsock);
- return(-4);
- }
-
-
- if (ioctl(mainsock, FIONBIO, &flag) == -1) {
- close(mainsock);
- return(-5);
- }
-
- Mysock.DaemonSock = mainsock;
- FD_SET(mainsock, &Mysock.readfds);
- FD_SET(mainsock, &Mysock.exceptfds);
- return(1);
- }
- void CloseMainSock()
-
- {
- close(Mysock.DaemonSock);
- FD_CLR(Mysock.DaemonSock, &Mysock.readfds);
- FD_CLR(Mysock.DaemonSock, &Mysock.exceptfds);
- }
-
- int CreateConnection(struct in_addr *sin_addr)
-
-
-
-
- {
- struct sockaddr_in server;
- int tmpsock, flag=1, i;
-
- if ((tmpsock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
- return(-1);
-
- server.sin_family = AF_INET;
- server.sin_port = Mysock.Port;
- server.sin_addr.s_addr = sin_addr->s_addr;
-
- if (ioctl(tmpsock, FIONBIO, &flag) == -1) {
- close(tmpsock);
- return(-2);
- }
-
-
- if (connect(tmpsock, (struct sockaddr *)&server, sizeof(server)) < 0) {
- if ((errno != EWOULDBLOCK) && (errno != EINPROGRESS)) {
-
-
- close(tmpsock);
- return(-3);
- }
- }
- FD_SET(tmpsock, &Mysock.readfds);
- FD_SET(tmpsock, &Mysock.writefds);
- FD_SET(tmpsock, &Mysock.exceptfds);
-
- i = 0;
- while (Mysock.Sockets[i] != 0) i++;
- if (i >= 64) {
- close(tmpsock);
- return(-4);
- }
-
- Mysock.Sockets[i] = tmpsock;
- Mysock.SockNum++;
- return(i);
- }
-
- int AcceptConnection(struct in_addr *IPaddr)
-
- {
- int newsock, len, flag=1, i;
- struct sockaddr_in addr;
-
- len = sizeof(addr);
- bzero((char *)&addr, len);
- if ((newsock = accept(Mysock.DaemonSock, &addr, &len)) == -1)
- return(-1);
-
-
- ioctl(newsock, FIONBIO, &flag);
-
- FD_SET(newsock, &Mysock.readfds);
- FD_SET(newsock, &Mysock.writefds);
- FD_SET(newsock, &Mysock.exceptfds);
-
-
- IPaddr->s_addr = addr.sin_addr.s_addr;
-
- i = 0;
- while (Mysock.Sockets[i] != 0) i++;
- if (i >= 64) {
- close(newsock);
- return(-4);
- }
-
- Mysock.Sockets[i] = newsock;
- Mysock.SockNum++;
- return(i);
- }
- int CloseConnection(int Sockno)
-
- {
- int retcode;
-
- if ((Sockno >= 64) || (Sockno < 0) || (Mysock.Sockets[Sockno] == 0))
- return(0);
- retcode = close(Mysock.Sockets[Sockno]);
- FD_CLR(Mysock.Sockets[Sockno], &Mysock.readfds);
- FD_CLR(Mysock.Sockets[Sockno], &Mysock.writefds);
- FD_CLR(Mysock.Sockets[Sockno], &Mysock.exceptfds);
-
- Mysock.Sockets[Sockno] = 0;
- Mysock.SockNum--;
- return(retcode);
- }
- int QuerySocketsMsg()
-
-
- {
- fd_set rfds, wfds, efds;
- int retcode, i;
- struct timeval TimeOut;
-
- rfds = Mysock.readfds;
- wfds = Mysock.writefds;
- efds = Mysock.exceptfds;
- TimeOut.tv_sec = 0;
- TimeOut.tv_usec = 0;
-
- bzero((char *)&SockMsg, sizeof(SockMsg));
- if ((retcode = select(64, &rfds, &wfds, &efds, &TimeOut)) == 0)
- return(0);
-
- if (FD_ISSET(Mysock.DaemonSock, &rfds))
- SockMsg.AcceptNum = 1;
-
- for (i=0; i<64; i++)
- {
- if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &rfds)))
- SockMsg.ReadQueue[SockMsg.ReadNum++] = i;
- }
-
- for (i=0; i<64; i++)
- {
- if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &wfds)))
- SockMsg.WriteQueue[SockMsg.WriteNum++] = i;
- }
-
- if (FD_ISSET(Mysock.DaemonSock, &efds))
- SockMsg.AcceptNum = -1;
-
- for (i=0; i<64; i++)
- {
- if ((Mysock.Sockets[i] > 0) && (FD_ISSET(Mysock.Sockets[i], &efds)))
- SockMsg.ExceptQueue[SockMsg.ExceptNum++] = i;
- }
- return(retcode);
- }
-
- int SendPacket(int Sockno, void *buf, int len)
-
- {
- int actlen;
-
- if ((Sockno >= 64) || (Sockno < 0) || (Mysock.Sockets[Sockno] == 0))
- return(0);
-
- if ((actlen = send(Mysock.Sockets[Sockno], buf, len, 0)) < 0)
- return(-1);
- return(actlen);
- }
-
- int RecvPacket(int Sockno, void *buf, int size)
-
-
- {
- int actlen;
- if ((Sockno >= 64) || (Sockno < 0) || (Mysock.Sockets[Sockno] == 0))
- return(0);
- if ((actlen = recv(Mysock.Sockets[Sockno], buf, size, 0)) < 0)
- return(0-errno);
- return(actlen);
- }
2.5.3 简单服务器程序示例
-
-
-
-
- #include "tcpsock.c"
- main(argc, argv)
- int argc;
- char **argv;
- {
- struct in_addr sin_addr;
- int retcode, i;
- char buf[32];
-
-
-
-
-
-
- (void) signal(SIGINT, CloseMainSock);
- (void) signal(SIGQUIT, CloseMainSock);
- (void) signal(SIGTERM, CloseMainSock);
-
- if ((retcode = InitPassiveSock("TestService")) < 0) {
- printf("InitPassiveSock: error code = %d\n", retcode);
- exit(-1);
- }
-
- while (1) {
- retcode = QuerySocketsMsg();
- if (SockMsg.AcceptNum == 1) {
- retcode = AcceptConnection(&sin_addr);
- printf("retcode = %d, IP = %s \n", retcode, inet_ntoa(sin_addr.s_addr));
- }
- else if (SockMsg.AcceptNum == -1)
- printf("Daemon Sockets error.\n");
- for (i=0; i<SockMsg.ReadNum; i++) {
- if ((retcode = RecvPacket(SockMsg.ReadQueue[i], buf, 32)) > 0)
- printf("sockno %d Recv string = %s \n", SockMsg.ReadQueue[i], buf);
- else
- CloseConnection(SockMsg.ReadQueue[i]);
- }
- }
- }
2.5.4 简单客户程序示例
-
-
-
-
-
-
-
- #include "tcpsock.h"
-
- main(argc, argv)
- int argc;
- char **argv;
- {
- char cmd_buf[16];
- struct in_addr sin_addr;
- int sockno1, retcode;
- char *buf = "This is a string for test.";
- sin_addr.s_addr = inet_addr("166.111.5.249");
-
- if ((retcode = InitSocketsStruct("TestService")) < 0) {
- printf("InitSocketsStruct: error code = %d\n", retcode);
- exit(1);
- }
-
- while (1) {
- printf(">");
- gets(cmd_buf);
- if (!strncmp(cmd_buf, "conn", 4)) {
- retcode = CreateConnection(&sin_addr);
- printf("return code: %d\n", retcode);
- }
- else if(!strncmp(cmd_buf, "send", 4)) {
- printf("Sockets Number:");
- scanf("%d", &sockno1);
- retcode = SendPacket(sockno1, buf, 26);
- printf("return code: %d\n", retcode, sizeof(buf));
- }
- else if (!strncmp(cmd_buf, "close", 4)) {
- printf("Sockets Number:");
- scanf("%d", &sockno1);
- retcode = CloseConnection(sockno1);
- printf("return code: %d\n", retcode);
- }
- else if (!strncmp(cmd_buf, "quit", 4))
- exit(0);
- else
- putchar('\007');
- }
- }
为了更好地说明套接字编程原理,下面给出几个基本套接字系统调用说明。
3.1 创建套接字──socket()
应用程序在使用套接字前,首先必须拥有一个套接字,系统调用socket()向应用程序提供创建套接字的手段,其调用格式如下:
- SOCKET PASCAL FAR socket(int af, int type, int protocol);
该调用要接收三个参数:af、type、protocol。参数af(地址族 address-family)指定通信发生的区域,UNIX系统支持的地址族有:AF_UNIX、AF_INET、AF_NS等,而DOS、WINDOWS中仅支持AF_INET(AF_INET:Internet协议族(TCP/IP)),它是网际网区域。因此,地址族与协议族相同。参数type 描述要建立的套接字的类型。参数protocol说明该套接字使用的特定协议,如果调用者不希望特别指定使用的协议,则置为0,使用默认的连接模式。根据这三个参数建立一个套接字,并将相应的资源分配给它,同时返回一个整型套接字号。因此,socket()系统调用实际上指定了相关五元组中的“协议”这一元。
3.2 指定本地地址──bind()
当一个套接字用socket()创建后,存在一个名字空间(地址族),但它没有被命名。bind()将套接字地址(包括本地主机地址和本地端口地址)与所创建的套接字号联系起来,即将名字赋予套接字,以指定本地半相关。其调用格式如下:
- int PASCAL FAR bind(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数s:是由socket()调用返回的并且未作连接的套接字描述符(套接字号)。
参数name: 是赋给套接字s的本地地址(名字),其长度可变,结构随通信域的不同而不同。
参数namelen:表明了name的长度。
如果没有错误发生,bind()返回0。否则返回值SOCKET_ERROR。
地址在建立套接字通信过程中起着重要作用,作为一个网络应用程序设计者对套接字地址结构必须有明确认识。例如,UNIX BSD有一组描述套接字地址的数据结构,其中使用TCP/IP协议的地址结构为:
- struct sockaddr_in{
- short sin_family;
- u_short sin_port;
- struct in_addr sin_addr;
- char sin_zero[8];
- }
3.3 建立套接字连接──connect()与accept()
这两个系统调用用于完成一个完整相关的建立,其中connect()用于建立连接。无连接的套接字进程也可以调用connect(),但这时在进程之间没有实际的报文交换,调用将从本地操作系统直接返回。这样做的优点是程序员不必为每一数据指定目的地址,而且如果收到的一个数据报,其目的端口未与任何套接字建立“连接”,便能判断该端口不可操作。而accept()用于使服务器等待来自某客户进程的实际连接。
connect()的调用格式如下:
- int PASCAL FAR connect(SOCKET s, const struct sockaddr FAR * name, int namelen);
参数s:是欲建立连接的本地套接字描述符。
参数name:指出说明对方套接字地址结构的指针。
namele:对方套接字地址长度由namelen说明。
如果没有错误发生,connect()返回0。否则返回值SOCKET_ERROR。在面向连接的协议中,该调用导致本地系统和外部系统之间连接实际建立。
由于地址族总被包含在套接字地址结构的前两个字节中,并通过socket()调用与某个协议族相关。因此bind()和connect()无须协议作为参数。
accept()的调用格式如下:
- SOCKET PASCAL FAR accept(SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen);
参数s:为本地套接字描述符,在用做accept()调用的参数前应该先调用过listen()。
参数addr: 指向客户方套接字地址结构的指针,用来接收连接实体的地址。addr的确切格式由套接字创建时建立的地址族决定。
参数addrlen: 为客户方套接字地址的长度(字节数)。如果没有错误发生,accept()返回一个SOCKET类型的值,表示接收到的套接字的描述符。否则返回值INVALID_SOCKET。
accept()用于面向连接服务器。参数addr和addrlen存放客户方的地址信息。调用前,参数addr 指向一个初始值为空的地址结构,而addrlen 的初始值为0;调用accept()后,服务器等待从编号为s的套接字上接受客户连接请求,而连接请求是由客户方的connect()调用发出的。当有连接请求到达时,accept()调用将请求连接队列上的第一个客户方套接字地址及长度放入addr 和addrlen,并创建一个与s有相同特性的新套接字号。新的套接字可用于处理服务器并发请求。
四个套接字系统调用,socket()、bind()、connect()、accept(),可以完成一个完全五元相关的建立。socket()指定五元组中的协议元,它的用法与是否为客户或服务器、是否面向连接无关。bind()指定五元组中的本地二元,即本地主机地址和端口号,其用法与是否面向连接有关:在服务器方,无论是否面向连接,均要调用bind();在客户方,若采用面向连接,则可以不调用bind(),而通过connect()自动完成。若采用无连接,客户方必须使用bind()以获得一个唯一的地址。
以上讨论仅对客户/服务器模式而言,实际上套接字的使用是非常灵活的,唯一需遵循的原则是进程通信之前,必须建立完整的相关。
3.4 监听连接──listen()
此调用用于面向连接服务器,表明它愿意接收连接。listen()需在accept()之前调用,其调用格式如下:
int PASCAL FAR listen(SOCKET s, int backlog);
参数s标识一个本地已建立、尚未连接的套接字号,服务器愿意从它上面接收请求。backlog表示请求连接队列的最大长度,用于限制排队请求的个数,目前允许的最大值为5。如果没有错误发生,listen()返回0。否则它返回SOCKET_ERROR。
listen()在执行调用过程中可为没有调用过bind()的套接字s完成所必须的连接,并建立长度为backlog的请求连接队列。
调用listen()是服务器接收一个连接请求的四个步骤中的第三步。它在调用socket()分配一个流套接字,且调用bind()给s赋于一个名字之后调用,而且一定要在accept()之前调用。
2.3节中提到客户/服务器模式中,有两种类型的服务:重复服务和并发服务。accept()调用为实现并发服务提供了极大方便,因为它要返回一个新的套接字号,其典型结构为:
- int initsockid, newsockid;
- if ((initsockid = socket(....)) < 0)
- error(“can’t create socket”);
- if (bind(initsockid,....) < 0)
- error(“bind error”);
- if (listen(initsockid , 5) < 0)
- error(“listen error”);
- for (; {
- newsockid = accept(initsockid, ...)
- if (newsockid < 0)
- error(“accept error“);
- if (fork() == 0){
- closesocket(initsockid);
- do(newsockid);
- exit(0);
- }
- closesocket(newsockid);
- }
这段程序执行的结果是newsockid与客户的套接字建立相关,子进程启动后,关闭继承下来的主服务器的initsockid,并利用新的newsockid与客户通信。主服务器的initsockid可继续等待新的客户连接请求。由于在Unix等抢先多任务系统中,在系统调度下,多个进程可以同时进行。因此,使用并发服务器可以使服务器进程在同一时间可以有多个子进程和不同的客户程序连接、通信。
客户程序看来,服务器可以同时并发地处理多个客户的请求,这就是并发服务器名称的来由。
面向连接服务器也可以是重复服务器,其结构如下:
- int initsockid, newsockid;
- if ((initsockid = socket(....))<0)
- error(“can’t create socket”);
- if (bind(initsockid,....)<0)
- error(“bind error”);
- if (listen(initsockid,5)<0)
- error(“listen error”);
- for (; {
- newsockid = accept(initsockid, ...)
- if (newsockid < 0)
- error(“accept error“);
- do(newsockid);
- closesocket(newsockid);
- }
重复服务器在一个时间只能和一个客户程序建立连接,它对多个客户程序的处理是采用循环的方式重复进行,因此叫重复服务器。并发服务器和重复服务器各有利弊:并发服务器可以改善客户程序的响应速度,但它增加了系统调度的开销;重复服务器正好与其相反,因此用户在决定是使用并发服务器还是重复服务器时,要根据应用的实际情考网考网来定。
3.5 数据传输──send()与recv()
当一个连接建立以后,就可以传输数据了。常用的系统调用有send()和recv()。
send()调用用于在s指定的已连接的数据报或流套接字上发送输出数据,格式如下:
- int PASCAL FAR send(SOCKET s, const char FAR *buf, int len, int flags);
参数s:为已连接的本地套接字描述符。
buf 指向存有发送数据的缓冲区的指针,
len :buf长度由len 指定。
flags 指定传输控制方式,如是否发送带外数据等。
如果没有错误发生,send()返回总共发送的字节数。否则它返回SOCKET_ERROR。
recv()调用用于在s指定的已连接的数据报或流套接字上接收输入数据,格式如下:
- int PASCAL FAR recv(SOCKET s, char FAR *buf, int len, int flags);
参数s 为已连接的套接字描述符。
buf:指向接收输入数据缓冲区的指针,
len :buf长度由len 指定。
flags 指定传输控制方式,如是否接收带外数据等。
如果没有错误发生,recv()返回总共接收的字节数。如果连接被关闭,返回0。否则它返回SOCKET_ERROR。
3.6 输入/输出多路复用──select()
select()调用用来检测一个或多个套接字的状态。对每一个套接字来说,这个调用可以请求读、写或错误状态方面的信息。请求给定状态的套接字集合由一个fd_set结构指示。在返回时,此结构被更新,以反映那些满足特定条件的套接字的子集,同时, select()调用返回满足条件的套接字的数目,其调用格式如下:
int PASCAL FAR select(int nfds, fd_set FAR * readfds, fd_set FAR * writefds, fd_set FAR * exceptfds, const struct timeval FAR * timeout);
参数nfds:指明被检查的套接字描述符的值域,此变量一般被忽略。
参数readfds:指向要做读检测的套接字描述符集合的指针,调用者希望从中读取数据。
参数writefds :指向要做写检测的套接字描述符集合的指针。
exceptfds指向要检测是否出错的套接字描述符集合的指针。
imeout指向select()函数等待的最大时间,如果设为NULL则为阻塞操作。
select()返回包含在fd_set结构中已准备好的套接字描述符的总数目,或者是发生错误则返回SOCKET_ERROR。
3.7 关闭套接字──closesocket()
closesocket()关闭套接字s,并释放分配给该套接字的资源;如果s涉及一个打开的TCP连接,则该连接被释放。closesocket()的调用格式如下:
- BOOL PASCAL FAR closesocket(SOCKET s);
参数s待关闭的套接字描述符。如果没有错误发生,closesocket()返回0。否则返回值SOCKET_ERROR。
4、TCP(socket建立)连接的时间
TCP建立连接,根据网络环境不同,使用的时间如下:
1、本机的话,通信内容直接走内存。
2、局域网走网卡,然后通过交换机,指定到相应主机。tcp建立3次握手,一般耗0.6毫秒左右。
3、外网通过网卡->路由器->互联网。
![](https://img-blog.csdn.net/20140803132610454?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvaGd1aXN1/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
5、TCP粘包的问题
1、什么是粘包:
简单的说就是通过TCP协议发送了多条独立的数据,但接收的时候,有些数据不幸的合并成了一个。比如客户端向服务器发送两个命令:
1)Hello
2)world
但是服务器接收的时候,很可能不是分两次接收,而是一次接收到 Helloworld
2、粘包出现原因:
由于TCP协议本身的机制(面向连接的可靠地协议-三次握手机制)客户端与服务器会维持一个连接(Channel),数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。那么这样的话,服务器在接收到消息(数据流)的时候就无法区分哪些数据包是客户端自己分开发送的,这样产生了粘包;服务器在接收到数据库后,放到缓冲区中,如果消息没有被及时从缓存区取走,下次在取数据的时候可能就会出现一次取出多个数据包的情况,造成粘包现象(确切来讲,对于基于TCP协议的应用,不应用包来描述,而应 用 流来描述),个人认为服务器接收端产生的粘包应该与Linux内核处理socket的方式 select轮询机制的线性扫描频度无关。
再说UDP:本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),他不会对数据包进行合并发送(也就没有Nagle算法之说了)
,他直接是一端发送什么数据,直接就发出去了
,既然他不会对数据合并,每一个数据包都是完整的(数据+UDP头+IP头等等发一次数据封装一次)也就没有粘包一说了。
也就是说:UDP不是流协议,有消息边界,不存在粘包的问题。要丢就是整个包都丢了。
原因:
1)、可能是IP分片传输导致的,也可能是传输过程中丢失部分包导致出现的半包
2)、为了提高传输速度和效率, 把发送缓冲区中的数据拼为一个数据包发送到目的地
比如:发送方需要等缓冲区满才发送出去,造成粘包
3)、接收方不及时接收缓冲区的包,造成多个包接收。
2、解决方案
:
1)、一个是采用分隔符的方式,即我们在封装要传输的数据包的时候,采用固定的符号作为结尾符(数据中不能含结尾符),这样我们接收到数据后,如果出现结尾标识,即人为的将粘包分开,如果一个包中没有出现结尾符,认为出现了分包,则等待下个包中出现后 组合成一个完整的数据包,这种方式适合于文本传输的数据,如采用/r/n之类的分隔符;
2)、另一种是采用在数据包中添加长度的方式,即在数据包中的固定位置封装数据包的长度信息(或可计算数据包总长度的信息),服务器接收到数据后,先是解析包长度,然后根据包长度截取数据包(此种方式常出现于自定义协议中),但是有个小问题就是如果客户端第一个数据包数据长度封装的有错误,那么很可能就会导致后面接收到的所有数据包都解析出错(由于TCP建立连接后流式传输机制),只有客户端关闭连接后重新打开才可以消除此问题,我在处理这个问题的时候对数据长度做了校验,会适时的对接收到的有问题的包进行人为的丢弃处理(客户端有自动重发机制,故而在应用层不会导致数据的不完整性);
3)另一种不建议的方式是TCP采用短连接处理粘包(这个得根据需要来,所以不建议)
6、TCP与UDP区别
TCP(Transmission Control Protocol)传输控制协议:
该协议主要用于在主机间建立一个虚拟连接,以实现高可靠性的数据包交换。IP协议可以进行IP数据包的分割和组装,但是通过IP协议并不能清楚地了解到数据包是否顺利地发送给目标计算机。而使用TCP协议就不同了,在该协议传输模式中在将数据包成功发送给目标计算机后,TCP会要求发送一个确认;如果在某个时限内没有收到确认,那么TCP将重新发送数据包。另外,在传输的过程中,如果接收到无序、丢失以及被破坏的数据包,TCP还可以负责恢复。
传输控制协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,通常由IETF的RFC793说明。在简化的计算机网络OSI模型中,它完成运输层所指定的功能。
TCP字节流:
打个比方比喻TCP,你家里有个蓄水池,你可以里面倒水,蓄水池上有个龙头,你可以通过龙头将水池里的水放出来,然后用各种各样的容器装(杯子、矿泉水瓶、锅碗瓢盆)接水。
上面的例子中,往水池里倒几次水和接几次水是没有必然联系的,也就是说你可以只倒一次水,然后分10次接完。另外,水池里的水接多少就会少多少;往里面倒多少水,就会增加多少水,但是不能超过水池的容量,多出的水会溢出。
结合TCP的概念,水池就好比接收缓冲区,倒水就相当于发送数据,接水就相当于读取数据。好比你通过TCP连接给另一端发送数据,你只调用了一次write,发送了100个字节,但是对方可以分10次收完,每次10个字节;你也可以调用10次write,每次10个字节,但是对方可以一次就收完。(假设数据都能到达)但是,你发送的数据量不能大于对方的接收缓存(流量控制),如果你硬是要发送过量数据,则对方的缓存满了就会把多出的数据丢弃。
UDP (User Datagram Protocol) 用户数据报协议:
用户数据报协议(UDP)是 ISO参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。 UDP 协议基本上是 IP 协议与上层协议的接口。 UDP协议适用端口分别运行在同一台设备上的多个应用程序。
由于大多数网络应用程序都在同一台机器上运行,计算机上必须能够确保目的地机器上的软件程序能从源地址机器处获得数据包,以及源计算机能收到正确的回复。这是通过使用UDP 的“端口号”完成的。
UDP数据报:
UDP和TCP不同,发送端调用了几次write,接收端必须用相同次数的read读完。UPD是基于报文的,在接收的时候,每次最多只能读取一个报文,报文和报文是不会合并的,如果缓冲区小于报文长度,则多出的部分会被丢弃。也就说,如果不指定MSG_PEEK标志,每次读取操作将消耗一个报文。
区别:
1、基于连接与无连接
TCP---传输控制协议提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。
每个数据包的传输过程是:先建立链路、数据传输、然后清除链路。数据包不包含目的地址。受端和发端不但顺序一致,而且内容相同。它的可靠性高。
UDP---用户数据报协议是面向无连接的,每个数据包都有完整的源、目的地址及分组编号,各自在网络中独立传输,传输中不管其顺序,数据到达收端后再进行排序组装,遇有丢失、差错和失序等情况,通过请求重发来解决。它的效率比较高。
UDP是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。
3、UDP程序结构较简单
4、流模式与数据报模式
5、TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证
6、TCP提供可靠的字节流服务 ,UDP 并不提供 对IP协议的可靠机制、流控制以及错误恢复功能等。