潍坊网站建设招聘,官方网站建设 在线磐石网络,织梦网站栏目对应首页,四川建设招标网站转载#xff1a;http://blog.csdn.net/hguisu/article/details/7445768/ Linux的SOCKET编程详解 1. 网络中进程之间如何通信 进 程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行#xff0c;为保证两个相互通信的进 程之间既互不干扰又协调一致工作http://blog.csdn.net/hguisu/article/details/7445768/ Linux的SOCKET编程详解 1. 网络中进程之间如何通信 进 程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行为保证两个相互通信的进 程之间既互不干扰又协调一致工作操作系统为进程通信提供了相应设施如 UNIX BSD有管道pipe、命名管道named pipe软中断信号signal UNIX system V有消息message、共享存储区shared memory和信号量semaphore)等. 他们都仅限于用在本机进程之间通信。网间进程通信要解决的是不同主机进程间的相互通信问题可把同机进程通信看作是其中的特例。为此首先要解决的是网间进程标识问题。同一主机上不同进程可用进程号process ID唯一标识。但在网络环境下各主机独立分配的进程号不能唯一标识该进程。例如主机A赋于某进程号5在B机中也可以存在5号进程因此“5号进程”这句话就没有意义了。 其次操作系统支持的网络协议众多不同协议的工作方式不同地址格式也不同。因此网间进程通信还要解决多重协议的识别问题。 其实TCP/IP协议族已经帮我们解决了这个问题网络层的“ip地址”可以唯一标识网络中的主机而传输层的“协议端口”可以唯一标识主机中的应用程序进程。这样利用三元组ip地址协议端口就可以标识网络的进程了网络中的进程通信就可以利用这个标志与其它进程进行交互。 使用TCP/IP协议的应用程序通常采用应用编程接口UNIX BSD的套接字socket和UNIX System V的TLI已经被淘汰来实现网络进程之间的通信。就目前而言几乎所有的应用程序都是采用socket而现在又是网络时代网络中进程通信是无处不在这就是我为什么说“一切皆socket”。 2. 什么是TCP/IP、UDP TCP/IPTransmission Control Protocol/Internet Protocol即传输控制协议/网间协议是一个工业标准的协议集它是为广域网WANs设计的。 TCP/IP协议存在于OS中网络服务通过OS提供在OS中增加支持TCP/IP的系统调用——Berkeley套接字如SocketConnectSendRecv等 UDPUser Data Protocol用户数据报协议是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。如图 TCP/IP协议族包括运输层、网络层、链路层而socket所在位置如图Socket是应用层与TCP/IP协议族通信的中间软件抽象层。 3. Socket是什么 1、 socket套接字 socket起源于Unix而Unix/Linux基本哲学之一就是“一切皆文件”都可以用“打开open – 读写write/read – 关闭close”模式来操作。Socket就是该模式的一个实现 socket即是一种特殊的文件一些socket函数就是对其进行的操作读/写IO、打开、关闭. 说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层它是一组接口。在设计模式中Socket其实就是一个门面模式它把复杂的TCP/IP协议族隐藏在Socket接口后面对用户来说一组简单的接口就是全部让Socket去组织数据以符合指定的协议。 注意其实socket也没有层的概念它只是一个facade设计模式的应用让编程变的更简单。是一个软件抽象层。在网络编程中我们大量用的都是通过socket实现的。 2、套接字描述符 其实就是一个整数我们最熟悉的句柄是0、1、2三个0是标准输入1是标准输出2是标准错误输出。0、1、2是整数表示的对应的FILE *结构的表示就是stdin、stdout、stderr 套接字API最初是作为UNIX操作系统的一部分而开发的所以套接字API与系统的其他I/O设备集成在一起。特别是当应用程序要为因特网通信而创建一个套接字socket时操作系统就返回一个小整数作为描述符descriptor来标识这个套接字。然后应用程序以该描述符作为传递参数通过调用函数来完成某种操作例如通过网络传送数据或接收输入的数据。 在许多操作系统中套接字描述符和其他I/O描述符是集成在一起的所以应用程序可以对文件进行套接字I/O或I/O读/写操作。 当应用程序要创建一个套接字时操作系统就返回一个小整数作为描述符应用程序则使用这个描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。操作系统就创建一个文件描述符提供给应用程序访问文件。从应用程序的角度看文件描述符是一个整数应用程序可以用它来读写文件。下图显示操作系统如何把文件描述符实现为一个指针数组这些指针指向内部数据结构。 对于每个程序系统都有一张单独的表。精确地讲系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时系统把一个指向此文件内部数据结构的指针写入文件描述符表并把该表的索引值返回给调用者 。应用程序只需记住这个描述符并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表通过指针找到保存该文件所有的信息的数据结构。 针对套接字的系统数据结构 1、套接字API里有个函数socket它就是用来创建一个套接字。套接字设计的总体思路是单个系统调用就可以创建任何套接字因为套接字是相当笼统的。一旦套接字创建后应用程序还需要调用其他函数来指定具体细节。例如调用socket将创建一个新的描述符条目 2、虽然套接字的内部数据结构包含很多字段但是系统创建套接字后大多数字字段没有填写。应用程序创建套接字后在该套接字可以使用之前必须调用其他的过程来填充这些字段。 3、文件描述符和文件指针的区别 文件描述符在linux系统中打开文件就会获得文件描述符它是个很小的正整数。每个进程在PCBProcess Control Block中保存着一份文件描述符表文件描述符就是这个表的索引每个表项都有一个指向已打开文件的指针。 文件指针C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引因此从某种意义上说文件指针就是句柄的句柄在Windows系统上文件描述符被称作文件句柄。 详细内容请看linux文件系统http://blog.csdn.net/hguisu/article/details/6122513#t7 4. 基本的SOCKET接口函数 在生活中A要电话给BA拨号B听到电话铃声后提起电话这时A和B就建立起了连接A和B就可以讲话了。等交流结束挂断电话结束此次交谈。 打电话很简单解释了这工作原理“open—write/read—close”模式。 服务器端先初始化Socket然后与端口绑定(bind)对端口进行监听(listen)调用accept阻塞等待客户端连接。在这时如果有个客户端初始化一个Socket然后连接服务器(connect)如果连接成功这时客户端与服务器端的连接就建立了。客户端发送数据请求服务器端接收请求并处理请求然后把回应数据发送给客户端客户端读取数据最后关闭连接一次交互结束。 这些接口的实现都是内核来完成。具体如何实现可以看看linux的内核 4.1、socket()函数 int socket(int protofamily, int type, int protocol);//返回sockfd sockfd是描述符。 socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字而socket()用于创建一个socket描述符socket descriptor它唯一标识一个socket。这个socket描述字跟文件描述字一样后续的操作都有用到它把它作为参数通过它来进行一些读写操作。 正如可以给fopen的传入不同参数值以打开不同的文件。创建socket的时候也可以指定不同的参数创建不同的socket描述符socket函数的三个参数分别为 protofamily即协议域又称为协议族family。常用的协议族有AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL或称AF_UNIXUnix域socket、AF_ROUTE等等。协议族决定了socket的地址类型在通信中必须采用对应的地址如AF_INET决定了要用ipv4地址32位的与端口号16位的的组合、AF_UNIX决定了要用一个绝对路径名作为地址。type指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等socket的类型有哪些。protocol故名思意就是指定协议。常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议这个协议我将会单独开篇讨论。 注意并不是上面的type和protocol可以随意组合的如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时会自动选择type类型对应的默认协议。 当我们调用socket创建一个socket时返回的socket描述字它存在于协议族address familyAF_XXX空间中但没有一个具体的地址。如果想要给它赋值一个地址就必须调用bind()函数否则就当调用connect()、listen()时系统会自动随机分配一个端口。 4.2、bind()函数 正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 函数的三个参数分别为 sockfd即socket描述字它是通过socket()函数创建了唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。addr一个const struct sockaddr *指针指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同如ipv4对应的是 struct sockaddr_in {sa_family_t sin_family; /* address family: AF_INET */in_port_t sin_port; /* port in network byte order */struct in_addr sin_addr; /* internet address */
};/* Internet address. */
struct in_addr {uint32_t s_addr; /* address in network byte order */
}; ipv6对应的是 struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */
};struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */
}; Unix域对应的是 #define UNIX_PATH_MAX 108struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */
}; addrlen对应的是地址的长度。 通常服务器在启动的时候都会绑定一个众所周知的地址如ip地址端口号用于提供服务客户就可以通过它来接连服务器而客户端就不用指定有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind()而客户端就不会调用而是在connect()时由系统随机生成一个。 网络字节序与主机字节序 主机字节序就是我们平常说的大端和小端模式不同的CPU有不同的字节序类型这些字节序是指整数在内存中保存的顺序这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下 a) Little-Endian就是低位字节排放在内存的低地址端高位字节排放在内存的高地址端。 b) Big-Endian就是高位字节排放在内存的低地址端低位字节排放在内存的高地址端。 网络字节序4个字节的32 bit值以下面的次序传输首先是07bit其次815bit然后1623bit最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序因此它又称作网络字节序。字节序顾名思义字节的顺序就是大于一个字节类型的数据在内存中的存放顺序一个字节的数据没有顺序的问题了。 所以在将一个地址绑定到socket的时候请先将主机字节序转换成为网络字节序而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案公司项目代码中由于存在这个问题导致了很多莫名其妙的问题所以请谨记对主机字节序不要做任何假定务必将其转化为网络字节序再赋给socket。 4.3、listen()、connect()函数 如果作为一个服务器在调用socket()、bind()之后就会调用listen()来监听这个socket如果客户端这时调用connect()发出连接请求服务器端就会接收到这个请求。 int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); listen函数的第一个参数即为要监听的socket描述字第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的listen函数将socket变为被动类型的等待客户的连接请求。 connect函数的第一个参数即为客户端的socket描述字第二参数为服务器的socket地址第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。 4.4、accept()函数 TCP服务器端依次调用socket()、bind()、listen()之后就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后就会调用accept()函数取接收请求这样连接就建立好了。之后就可以开始网络I/O操作了即类同于普通文件的读写I/O操作。 int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd 参数sockfd参数sockfd就是上面解释中的监听套接字这个套接字用来监听一个端口当有一个客户与服务器连接时它使用这个一个端口号而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节它只知道一个地址和一个端口号。参数addr这是一个结果参数它用来接受一个返回值这返回值指定客户端的地址当然这个地址是通过某个地址结构来描述的用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣那么可以把这个值设置为NULL。参数len如同大家所认为的它也是结果的参数用来接受上述addr的结构的大小的它指明addr结构所占有的字节个数。同样的它也可以被设置为NULL。 如果accept成功返回则服务器与客户已经正确建立连接了此时服务器通过accept返回的套接字来完成与客户的通信。 注意 accept默认会阻塞进程直到有一个客户连接建立后返回它返回的是一个新可用的套接字这个套接字是连接套接字。 此时我们需要区分两种套接字 监听套接字: 监听套接字正如accept的参数sockfd它是监听套接字在调用listen函数之后是服务器开始调用socket()函数生成的称为监听socket描述字(监听套接字) 连接套接字一个套接字会从主动连接的套接字变身为一个监听套接字而accept函数返回的是已连接socket描述字(一个连接套接字)它代表着一个网络已经存在的点点连接。 一个服务器通常通常仅仅只创建一个监听socket描述字它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字当服务器完成了对某个客户的服务相应的已连接socket描述字就被关闭。 自然要问的是为什么要有两种套接字原因很简单如果使用一个描述字的话那么它的功能太多使得使用很不直观同时在内核确实产生了一个这样的新的描述字。 连接套接字socketfd_new 并没有占用新的端口与客户端通信依然使用的是与监听套接字socketfd一样的端口号 4.5、read()、write()等函数 万事具备只欠东风至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了即实现了网咯中不同进程之间的通信网络I/O操作有下面几组 read()/write()recv()/send()readv()/writev()recvmsg()/sendmsg()recvfrom()/sendto() 我推荐使用recvmsg()/sendmsg()函数这两个函数是最通用的I/O函数实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下 #include unistd.hssize_t read(int fd, void *buf, size_t count);ssize_t write(int fd, const void *buf, size_t count);#include sys/types.h#include sys/socket.hssize_t send(int sockfd, const void *buf, size_t len, int flags);ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);read函数是负责从fd中读取内容.当读成功时read返回实际所读的字节数如果返回的值是0表示已经读到文件的结束了小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的如果是ECONNREST表示网络连接出了问题。 write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1并设置errno变量。 在网络程序中当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0表示写了部分或者是全部的数据。2)返回的值小于0此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。 其它的我就不一一介绍这几对I/O函数了具体参见man文档或者baidu、Google下面的例子中将使用到send/recv。 4.6、close()函数 在服务器与客户端建立连接之后会进行一些读写操作完成了读写操作就要关闭相应的socket描述字好比操作完打开的文件要调用fclose关闭打开的文件。 #include unistd.h
int close(int fd); close一个TCP socket的缺省行为时把该socket标记为以关闭然后立即返回到调用进程。该描述字不能再由调用进程使用也就是说不能再作为read或write的第一个参数。 注意close操作只是使相应socket描述字的引用计数-1只有当引用计数为0的时候才会触发TCP客户端向服务器发送终止连接请求。 5. Socket中TCP的建立三次握手 TCP协议通过三个报文段完成连接的建立这个过程称为三次握手(three-way handshake)过程如下图所示。 第一次握手建立连接时客户端发送syn包(synj)到服务器并进入SYN_SEND状态等待服务器确认SYN同步序列编号(Synchronize Sequence Numbers)。 第二次握手服务器收到syn包必须确认客户的SYNackj1同时自己也发送一个SYN包synk即SYNACK包此时服务器进入SYN_RECV状态 第三次握手客户端收到服务器的SYNACK包向服务器发送确认包ACK(ackk1)此包发送完毕客户端和服务器进入ESTABLISHED状态完成三次握手。 一个完整的三次握手也就是 请求---应答---再次确认。 对应的函数接口 从图中可以看出当客户端调用connect时触发了连接请求向服务器发送了SYN J包这时connect进入阻塞状态服务器监听到连接请求即收到SYN J包调用accept函数接收请求向客户端发送SYN K ACK J1这时accept进入阻塞状态客户端收到服务器的SYN K ACK J1之后这时connect返回并对SYN K进行确认服务器收到ACK K1时accept返回至此三次握手完毕连接建立。 我们可以通过网络抓包的查看具体的流程 比如我们服务器开启9502的端口。使用tcpdump来抓包 tcpdump -iany tcp port 9502 然后我们使用telnet 127.0.0.1 9502开连接.: telnet 127.0.0.1 9502 14:12:45.104687 IP localhost.39870 localhost.9502: Flags [S], seq 2927179378, win 32792, options [mss 16396,sackOK,TS val 255474104 ecr 0,nop,wscale 3], length 01 14:12:45.104701 IP localhost.9502 localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, win 32768, options [mss 16396,sackOK,TS val 255474104 ecr 255474104,nop,wscale 3], length 0 2 14:12:45.104711 IP localhost.39870 localhost.9502: Flags [.], ack 1, win 4099, options [nop,nop,TS val 255474104 ecr 255474104], length 0 3 14:13:01.415407 IP localhost.39870 localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7 14:13:01.415432 IP localhost.9502 localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0 14:13:01.415747 IP localhost.9502 localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18 14:13:01.415757 IP localhost.39870 localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0 114:12:45.104687 时间带有精确到微妙localhost.39870 localhost.9502 表示通信的流向39870是客户端9502是服务器端[S] 表示这是一个SYN请求[S.] 表示这是一个SYNACK确认包: [.] 表示这是一个ACT确认包 (client)SYN-(server)SYN-(client)ACT 就是3次握手过程[P] 表示这个是一个数据推送可以是从服务器端向客户端推送也可以从客户端向服务器端推[F] 表示这是一个FIN包是关闭连接操作client/server都有可能发起[R] 表示这是一个RST包与F包作用相同但RST表示连接关闭时仍然有数据未被处理。可以理解为是强制切断连接win 4099 是指滑动窗口大小length 18指数据包的大小 我们看到 123三步是建立tcp 第一次握手 14:12:45.104687 IP localhost.39870 localhost.9502: Flags [S], seq 2927179378 客户端IP localhost.39870 (客户端的端口一般是自动分配的) 向服务器localhost.9502 发送syn包(synj)到服务器》 syn包(synj) syn的seq 2927179378 j2927179378 第二次握手 14:12:45.104701 IP localhost.9502 localhost.39870: Flags [S.], seq 1721825043, ack 2927179379, 收到请求并确认服务器收到syn包并必须确认客户的SYNackj1同时自己也发送一个SYN包synk即SYNACK包 此时服务器主机自己的SYNseqy syn seq 1721825043。 ACK为j1 ackj1ack 2927179379 第三次握手 14:12:45.104711 IP localhost.39870 localhost.9502: Flags [.], ack 1, 客户端收到服务器的SYNACK包向服务器发送确认包ACK(ackk1) 客户端和服务器进入ESTABLISHED状态后可以进行通信数据交互。此时和accept接口没有关系即使没有accepte也进行3次握手完成。 连接出现连接不上的问题一般是网路出现问题或者网卡超负荷或者是连接数已经满啦。 紫色背景的部分 IP localhost.39870 localhost.9502: Flags [P.], seq 1:8, ack 1, win 4099, options [nop,nop,TS val 255478182 ecr 255474104], length 7 客户端向服务器发送长度为7个字节的数据 IP localhost.9502 localhost.39870: Flags [.], ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 0 服务器向客户确认已经收到数据 IP localhost.9502 localhost.39870: Flags [P.], seq 1:19, ack 8, win 4096, options [nop,nop,TS val 255478182 ecr 255478182], length 18 然后服务器同时向客户端写入数据。 IP localhost.39870 localhost.9502: Flags [.], ack 19, win 4097, options [nop,nop,TS val 255478182 ecr 255478182], length 0 客户端向服务器确认已经收到数据 这个就是tcp可靠的连接每次通信都需要对方来确认。 6. TCP连接的终止四次握手释放 建立一个连接需要三次握手而终止一个连接要经过四次握手这是由TCP的半关闭(half-close)造成的如图 由于TCP连接是全双工的因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭而另一方执行被动关闭。 1客户端A发送一个FIN用来关闭客户A到服务器B的数据传送报文段4。 2服务器B收到这个FIN它发回一个ACK确认序号为收到的序号加1报文段5。和SYN一样一个FIN将占用一个序号。 3服务器B关闭与客户端A的连接发送一个FIN给客户端A报文段6。 4客户端A发回ACK报文确认并将确认序号设置为收到序号加1报文段7。 对应函数接口如图 过程如下 某个应用进程首先调用close主动关闭连接这时TCP发送一个FIN M 另一端接收到FIN M之后执行被动关闭对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据 一段时间之后接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N 接收到这个FIN的源发送端TCP对它进行确认。 这样每个方向上都有一个FIN和ACK。 1为什么建立连接协议是三次握手而关闭连接却是四次握手呢 这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后它可以把ACK和SYNACK起应答作用而SYN起同步作用放在一个报文里来发送。但关闭连接时当收到对方的FIN报文通知时它仅仅表示对方没有数据发送给你了但未必你所有的数据都全部发送给对方了所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后再发送FIN报文给对方来表示你同意现在可以关闭连接了所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。 2为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态 这是因为虽然双方都同意关闭连接了而且握手的4个报文也都协调和发送完毕按理可以直接回到CLOSED状态就好比从SYN_SEND状态到ESTABLISH状态那样但是因为我们必须要假想网络是不可靠的你无法保证你最后发送的ACK报文会一定被对方收到因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文而重发FIN报文所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。 7. Socket编程实例 服务器端一直监听本机的8000号端口如果收到连接请求将接收请求并接收客户端发来的消息并向客户端返回消息。 [cpp] view plaincopy print? /* File Name: server.c */ #includestdio.h #includestdlib.h #includestring.h #includeerrno.h #includesys/types.h #includesys/socket.h #includenetinet/in.h #define DEFAULT_PORT 8000 #define MAXLINE 4096 int main(int argc, char** argv) { int socket_fd, connect_fd; struct sockaddr_in servaddr; char buff[4096]; int n; //初始化Socket if( (socket_fd socket(AF_INET, SOCK_STREAM, 0)) -1 ){ printf(create socket error: %s(errno: %d)\n,strerror(errno),errno); exit(0); } //初始化 memset(servaddr, 0, sizeof(servaddr)); servaddr.sin_family AF_INET; servaddr.sin_addr.s_addr htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。 servaddr.sin_port htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT //将本地地址绑定到所创建的套接字上 if( bind(socket_fd, (struct sockaddr*)servaddr, sizeof(servaddr)) -1){ printf(bind socket error: %s(errno: %d)\n,strerror(errno),errno); exit(0); } //开始监听是否有客户端连接 if( listen(socket_fd, 10) -1){ printf(listen socket error: %s(errno: %d)\n,strerror(errno),errno); exit(0); } printf(waiting for clients request\n); while(1){ //阻塞直到有客户端连接不然多浪费CPU资源。 if( (connect_fd accept(socket_fd, (struct sockaddr*)NULL, NULL)) -1){ printf(accept socket error: %s(errno: %d),strerror(errno),errno); continue; } //接受客户端传过来的数据 n recv(connect_fd, buff, MAXLINE, 0); //向客户端发送回应数据 if(!fork()){ /*紫禁城*/ if(send(connect_fd, Hello,you are connected!\n, 26,0) -1) perror(send error); close(connect_fd); exit(0); } buff[n] \0; printf(recv msg from client: %s\n, buff); close(connect_fd); } close(socket_fd); } 客户端 [cpp] view plaincopy print? /* File Name: client.c */ #includestdio.h #includestdlib.h #includestring.h #includeerrno.h #includesys/types.h #includesys/socket.h #includenetinet/in.h #define MAXLINE 4096 int main(int argc, char** argv) { int sockfd, n,rec_len; char recvline[4096], sendline[4096]; char buf[MAXLINE]; struct sockaddr_in servaddr; if( argc ! 2){ printf(usage: ./client ipaddress\n); exit(0); } if( (sockfd socket(AF_INET, SOCK_STREAM, 0)) 0){ printf(create socket error: %s(errno: %d)\n, strerror(errno),errno); exit(0); } memset(servaddr, 0, sizeof(servaddr)); servaddr.sin_family AF_INET; servaddr.sin_port htons(8000); if( inet_pton(AF_INET, argv[1], servaddr.sin_addr) 0){ printf(inet_pton error for %s\n,argv[1]); exit(0); } if( connect(sockfd, (struct sockaddr*)servaddr, sizeof(servaddr)) 0){ printf(connect error: %s(errno: %d)\n,strerror(errno),errno); exit(0); } printf(send msg to server: \n); fgets(sendline, 4096, stdin); if( send(sockfd, sendline, strlen(sendline), 0) 0) { printf(send msg error: %s(errno: %d)\n, strerror(errno), errno); exit(0); } if((rec_len recv(sockfd, buf, MAXLINE,0)) -1) { perror(recv error); exit(1); } buf[rec_len] \0; printf(Received : %s ,buf); close(sockfd); exit(0); } inet_pton 是Linux下IP地址转换函数可以在将IP地址在“点分十进制”和“整数”之间转换 是inet_addr的扩展。 [cpp] view plaincopy print? int inet_pton(int af, const char *src, void *dst);//转换字符串到网络地址: 第一个参数af是地址族转换后存在dst中 af AF_INET:src为指向字符型的地址即ASCII的地址的首地址ddd.ddd.ddd.ddd格式的函数将该地址转换为in_addr的结构体并复制在*dst中 af AF_INET6:src为指向IPV6的地址函数将该地址转换为in6_addr的结构体并复制在*dst中 如果函数出错将返回一个负值并将errno设置为EAFNOSUPPORT如果参数af指定的地址族和src格式不对函数将返回0。 测试 编译server.c gcc -o server server.c 启动进程 ./server 显示结果 waiting for clients request 并等待客户端连接。 编译 client.c gcc -o client server.c 客户端去连接server ./client 127.0.0.1 等待输入消息 发送一条消息输入c 此时服务器端看到 客户端收到消息 其实可以不用client,可以使用telnet来测试 telnet 127.0.0.1 8000