Socket编程学习总结

     读书的时候,每当遇到需要网路通信的时候,总是随便百度一下,然后就随便写写就过去了,对于socket本身并没有太深的认识。因为目前的工作中用到了网络通信,并且在过去写的socket通信一般都是阻塞的,这在项目中是无法忍受的。

     因此,本文主要参考网上的多篇博客,然后系统的总结了socket的概念,并给出了编程实例,不喜勿喷。

网络通信概念

     网络层的ip地址可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

     我们平常提到的HTTP、DNS、、FTP等等都是应用层的协议,传输层的协议主要有TCP(面向连接的可靠的)和UDP(面向无连接不可靠的),具体如下图:

TCP/IP协议族包括运输层、网络层、链路层,而socket所在位置如图,Socket是应用层与TCP/IP协议族通信的中间软件抽象层

     文件描述符,在网络编程中经常提及这个词,当时初学时一直就这么叫着,现在回头看。不过对Linux内核分配的IO的称谓而已,套接字(Socket)本质上就是文件描述符,为何加上文件两个字?因为Linux万物皆文件啊!

 

 

Socket概念

Socket基本概念

     socket是一种特殊文件,说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。

     在设计模式中,Socket其实就是一个外观模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面。对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。其实socket也没有层的概念,它只是一个外观设计模式的应用,我们大量用的都是通过socket实现的。

     我们平常说的TCP、UDP是指的行业规范好的通信协议,它们体现为具体的编程模型就是socket编程。socket是编程接口,TCP和UDP是通信规范,二者相互对应,但不是一个层次的。所以假如面试有人问你socket和TCP的区别,面试官其实就是考察你对协议和接口的理解。

 

Socket套接字描述符

     本质就是一个整数,我们最熟悉的句柄是0、1、2三个,0是标准输入,1是标准输出,2是标准错误输出。0、1、2是整数表示的,对应的FILE *结构的表示就是stdin、stdout、stderr。

     当应用程序要创建一个套接字时,操作系统就返回一个小整数作为描述符,应用程序则使用这个描述符来引用该套接字需要I/O请求的应用程序请求操作系统打开一个文件。

     从应用程序的角度看,文件描述符是一个整数,应用程序可以用它来读写文件。下图显示,操作系统如何把文件描述符实现为一个指针数组,这些指针指向内部数据结构。

     系统为每个运行的进程维护一张单独的文件描述符表。当进程打开一个文件时,系统把一个指向此文件内部数据结构的指针写入文件描述符表,并把该表的索引值返回给调用者。应用程序只需记住这个描述符,并在以后操作该文件时使用它。操作系统把该描述符作为索引访问进程描述符表,通过指针找到保存该文件所有的信息的数据结构。

     Linux有个lsof的命令能够查看某个进程所打开的文件描述符,它其实就是打印整个进程的文件描述符表。对于每个socket套接字,它内部包含很多字段,它的数据结构如下:

     套接字内部数据结构包含很多字段,但是大多数字段没有填写。应用程序创建套接字后在该套接字可以使用之前,必须调用其他的过程来填充这些字段。

 

       那么有个问题:文件描述符和文件指针的区别是什么呢?

     1)文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。

     2)文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄。

 

Socket接口函数

       服务器建立连接的流程和涉及到的函数:socket()、bind()、listen()、accept()、connect()、close()。结构体struct sockaddr_in也即网络通讯五元组,本端IP,本端端口、对端IP、对端端口、协议类型。

socket()函数

       函数原型如下:

     int socket(int protofamily, int type, int protocol);//返回sockfd

     创建一个套接字,并且设置该套接字协议类型,三个参数分别为:

     protofamily:即协议域。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域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传输协议。

     返回值sockfd是描述符,用于创建一个socket描述符,它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它作为参数。

 

connect()函数

       函数原型如下:

     int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

     发送连接请求,代码默认阻塞。第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。返回值为0表示连接成功,否者为失败。

 

bind()函数

     函数原型如下:

     int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

     为套接字绑定IP和端口。

     函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

     注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

     当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。Bind函数本质上是绑定一个端口和地址,也即我自己占用了这个地址,别人不允许用。函数三个参数分别为:

     sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。

     addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:

     返回值为0表示绑定成功,小于0表示失败。

 

     通常服务器启动时都会绑定一个众所周知的地址(如ip地址+端口号),客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

 

 

listen()函数

       函数原型如下:

     int listen(int sockfd, int backlog);

      listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。返回值为0表示监听成功,否者为失败。

     listen通过socket套接字和该套接字绑定的IP信息在内核开启监听,并且返回监听描述符。此处监听工作交给内核处理,代码本身不阻塞,但内核对应端口一直在做监听工作。同时维护两个连接队列,大小由backlog决定。

 

 

accept()函数

     TCP服务器监听到这个请求之后调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作。函数原型如下:

     int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd

     参数sockfd就是上面的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。

     参数addr:这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。

     参数len:它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。

     如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。

     注意:accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,这个套接字是连接套接字。此时我们需要区分两种套接字,

     1)监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字。

     2)连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字,它代表着一个网络已经存在的点对点连接。

     一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

     为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号。

 

 

read()和write()等读写函数

     网络I/O操作有下面几组:

     read()/write() && recv()/send() && readv()/writev()

     recvmsg()/sendmsg() && recvfrom()/sendto()

     推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

     read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

     write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

close()函数

     函数原型如下:

     int close(int fd);

     关闭描述符。listen描述符和accept描述符是完全独立的,关闭其中一个互不影响。

     close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。

 

 

TCP、UDP和socket的对应关系图

基于TCP的C/S程序一般流程

     服务器端调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态。

     客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。这里就是TCP的三次握手

 

 

     数据传输的过程:

     建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。

     如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。

     在学习socket API时要注意应用程序和TCP协议层是如何交互的:比如调用connect()会发出SYN段,应用程序如何知道TCP协议层的状态变化,比如从某个阻塞的socket函数返回就表明TCP协议收到了某些段,再比如read()返回0就表明收到了FIN段。

 

 

基于UDP的C/S程序一般流程

       对应UDP,它和TCP的过程类似,具体如下:

       服务器端:创建socket()、绑定bind()本机ip、先接受数据recvfrom()得到对方的ip和port、发送sendto()数据给对方、关闭close()

       客户端:创建socket()、发送数据sendto()到目标服务器、接受recvfrom()来自目标服务器数据、关闭close()

       具体过程如图:

 

 

相关问题

网络字节序与主机字节序

     主机字节序就是我们平常说的大端和小端模式:这些字节序是指整数在内存中保存的顺序,这个叫做主机序。小端是低字节放低地址,高字节放高地址,大端相反。

     网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

     所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是大端。由于这个问题曾引发过血案!所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。

     常用的字节序转换函数有:

     htonl: 转换long类型到网络字节序

     htons: 转换short类型到网络字节序

     ntohl: 转换网络字节序到long类型

     ntohs: 转换网络字节序到short类型

     inet_addr: 将字符串格式IP转换到网络字节序

     inet_ntoa: 将网络字节序格式IP转换到字符串

实例代码:

     SOCKADDR_IN addr;

     addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

     addr.sin_port = htons(5120);

     addr.sin_family = AF_INET;

 

 

 

TCP和socket接口函数的关系?

三次握手建立连接

     这里再详细阐述listen()调用后,内核是如何维护两个连接队列的。其实维护的就是熟悉的三次握手过程。

 

     Client请求时间是不确定的,当多个请求到Server时,处于请求队列,等待listen的端口逐个处理至就绪队列。

     connect处于阻塞态等待请求从listen的就绪队列被accept调度返回具体用于数据传输的accept_fd描述符。

     accept处于阻塞态,当请求队列为空或处理完毕时。

     由此可知,三次握手由connet发起,accept结束,途中经历listen的队列维护。

     accept实际上是在从内核listen维护的就绪队列中取描述符。第二次握手之后connect返回,第三次握手accept返回。如下图:

 

 

四次挥手关闭连接

     某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;接收到这个FIN的源发送端TCP对它进行确认。

 

 

listen()函数的作用是什么?

     int listen(int sockfd, int backlog);

     listen函数仅由TCP服务器调用,它做两件事情:

  1. :listen函数使用主动连接套接口变为被连接套接口,使得一个进程可以接受其它进程的请求,从而成为一个服务器进程。在TCP服务器编程中listen函数把进程变为一个服务器,并指定相应的套接字变为被动连接。根据TCP状态转换图,调用listen导致套接口从CLOSED状态转换到LISTEN状态。

     2、本函数的第二个参数规定了内核应该为相应套接口排队的最大连接个数。服务器进程不能随便指定一个数值,内核有一个许可的范围。这个范围是实现相关的。很难有某种统一,一般这个值会小30以内。

 

     为了更好的理解backlog参数,我们必须认识到内核为任何一个给定的监听套接口维护两个队列:

     1、未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接口处于SYN_RCVD状态。

     2、已完成连接队列(completed connection queue),每个已完成TCP三路握手过程的客户对应其中一项。这些套接口处于ESTABLISHED状态。

 

     当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三路握手的第二个分节:服务器的SYN响应,其中稍带对客户SYN的ACK(即SYN+ACK)。这一项一直保留在未完成连接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止(曾经源自Berkeley的实现为这些未完成连接的项设置的超时值为75秒)。如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。

 

 

accept()函数是干什么的呢?

     int accept(int s,struct sockaddr * addr,int * addrlen)

     accept()用来接受参数s的socket连接。

     服务程序调用accept函数从处于监听状态的流套接字s的客户连接请求队列中取出排在最前的一个客户请求,并且创建一个新的套接字来与客户套接字创建连接通道,如果连接成功,就返回新创建的套接字的描述符,以后与客户套接字交换数据的是新创建的套接字;如果失败就返回 INVALID_SOCKET。该函数的第一个参数指定处于监听状态的流套接字;操作系统利用第二个参数来返回新创建的套接字的地址结构;操作系统利用第三个参数来返回新创建的套接字的地址结构的长度。

 

 

send或者write socket遭遇SIGPIPE信号?

     我写了一个服务器程序, 在Linux下测试时, 总是莫名退出. 最后跟踪到是write调用导致退出. 用gdb执行程序, 退出时提示"Broken pipe"。最后问题确定为, 对一个对端已经关闭的socket调用两次write, 第二次将会生成SIGPIPE信号, 该信号默认结束进程.

     具体的分析可以结合TCP的"四次握手"关闭. TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端只是收到FIN包. 按照TCP协议的语义, 表示对端只是关闭了其所负责的那一条单工信道, 仍然可以继续接收数据. 也就是说, 因为TCP协议的限制, 一个端点无法获知对端的socket是调用了close还是shutdown.

     对一个已经收到FIN包的socket调用read方法, 如果接收缓冲已空, 则返回0, 这就是常说的表示连接关闭. 但第一次对其调用write方法时, 如果发送缓冲没问题, 会返回正确写入(发送). 但发送的报文会导致对端发送RST报文, 因为对端的socket已经调用了close, 完全关闭, 既不发送, 也不接收数据. 所以, 第二次调用write方法(假设在收到RST之后), 会生成SIGPIPE信号, 导致进程退出。

     为了避免进程退出, 可以捕获SIGPIPE信号, 或者忽略它, 给它设置SIG_IGN信号处理函数:

     signal(SIGPIPE, SIG_IGN);

     这样, 第二次调用write方法时, 会返回-1, 同时errno置为SIGPIPE. 程序便能知道对端已经关闭.

     PS: Linux下的SIGALRM似乎会每1秒钟往后偏移1毫秒, 但Windows下经过测试完全准时, 不差1毫秒.

当然还有其他方法来处理SIGPIPE,设置当前socket在进行写操作时不产生SIGPIPE。

     int set = 1;

     setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void  *)&set, sizeof(int));

     这样做的好处在于:在某些情况 下我们并不需要一个全局的SIGPIPE handler。但是据说这种并不通用,在linux下没有相应的定义—-但我在mac下测试通过。

 

     终止一个TCP连接的正常方式是发送FIN。在发送缓冲区中所有排队数据都已发送之后才发送FIN,正常情况下没有任何数据丢失。但我们有时也可能发送一个RST报文段而不是FIN来中途关闭一个连接。这称为异常关闭。现在知道RST报文的作用了,那就在大致列一下出现RST报文的场景吧:

     1.connect一个不存在的端口;

     2.向一个已经关掉的连接send数据;

     3.向一个已经崩溃的对端发送数据(连接之前已经被建立);

     4.close(sockfd)时,直接丢弃接收缓冲区未读取的数据,并给对方发一个RST。这个是由SO_LINGER选项来控制的;

     5.a重启,收到b的保活探针,a发rst,通知b。

 

 

Socket网络通信实例

基于TCP的C/S程序实例

     服务器端

       代码流程如下:

     1.创建服务器socket

     2.初始化端口和ip地址调用bind进行绑定

     3.调用listen进行监听

     4.调用accept接收客户端的请求

     5.调用recv和send与客户端进行通信

     6.调用close关闭网络环境和socket

     具体示例代码如下:

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <errno.h>

#include <string.h>



#define SRVIP "127.0.0.1"

#define SRVPORT 10005

#define MAX_NUM 80



int main()

{

   int serverSock = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);

    if(serverSock < 0)

    {

        printf("socket creation failed\n");

        exit(-1);

    }

    printf("socket create successfully.\n");



    struct sockaddr_in serverAddr;

    memset(&serverAddr,0,sizeof(serverAddr));

    serverAddr.sin_family=AF_INET;

    serverAddr.sin_port = htons((u_short)SRVPORT);

    serverAddr.sin_addr.s_addr = inet_addr(SRVIP);



    // bind() 将一个socket绑定到指定的IP和端口上,socket只接受绑定IP和端口的数据

    if(bind(serverSock, (struct sockaddr*)&serverAddr, sizeof(struct sockaddr))==-1)

    {

        printf("Bind error.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);

        exit(-1);

    }

    printf("Bind successful.IP[%s], Port[%d]\n", SRVIP, serverAddr.sin_port);



    // 当socket和一个地址绑定之后,listen()函数会开始监听可能的连接请求

    if(listen(serverSock,10)==-1)

    {

        printf("Listen error!\n");

    }

    printf("Listening on port[%d]\n", serverAddr.sin_port);



    char recvBuf[MAX_NUM]={0};

    char sendBuf[MAX_NUM]={0};

    while(1)

    {

        struct sockaddr clientAddr;

        int size = sizeof(clientAddr);



        //阻塞,直到有新tcp客户端连接

        int clientSock = accept(serverSock, &clientAddr, &size);

        printf("***SYS***    New client touched.\n");

        while(1)

        {

            //一直接收客户端socket的send操作

            if(recv(clientSock, recvBuf, MAX_NUM, 0) == -1)

            {

                printf("read error.\n");

            }

            else

            {

                printf("receiv from client:%s\n",recvBuf);

            }

            if(strcmp(recvBuf,"Quit") == 0 || strcmp(recvBuf,"quit") == 0)

            {

                strcpy(sendBuf, "Goodbye,my dear client!");

            }

            else

            {

                strcpy(sendBuf, "Hello Client.");

            }



            // 向客户端发送套接字

            if(send(clientSock, sendBuf , sizeof(sendBuf), MSG_NOSIGNAL) == -1)

            {

                printf("Send error!\n");

            }

            else

            {

                printf("Send to client:%s\n", sendBuf);

            }

            if(strcmp(recvBuf, "Quit") == 0 || strcmp(recvBuf, "quit") == 0)

            {

                break;

            }

            memset(recvBuf, 0, sizeof(recvBuf));

            memset(sendBuf, 0, sizeof(sendBuf));

        }

        close(clientSock);

    }

    close(serverSock);

    return 0;

}



     客户端

       代码流程如下:

     1.创建服务器socket

     2.主动connect服务器

     3.调用recv和send与客户端进行通信

     4.调用close关闭网络环境和socket

     具体示例代码如下:


 

#include <stdlib.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <errno.h>

#include <string.h>

#include   <unistd.h>

           

#define CLTIP "127.0.0.1"

#define SRVPORT 10005

#define MAX_NUM 80



int main()

{

    // 延迟1s

    //sleep(1000);



    // socket() 为通讯创建一个端点,为套接字返回一个文件描述符

    int clientsock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    if(clientsock < 0)

    {

        printf("socket creation failed\n");

        exit(-1);

    }

    printf("socket create successfully.\n");



    struct sockaddr_in clientAddr;

    clientAddr.sin_family = AF_INET;

    clientAddr.sin_port = htons((u_short)SRVPORT);

    clientAddr.sin_addr.s_addr = inet_addr(CLTIP);



    if(connect(clientsock, (struct sockaddr*)&clientAddr, sizeof(struct sockaddr)) < 0)

    {

        printf("Connect error.IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);

        exit(-1);

    }

    printf("Connect to IP[%s], port[%d]\n", CLTIP, clientAddr.sin_port);



    char sendBuf[MAX_NUM]={0};

    char recvBuf[MAX_NUM]={0};

    while(gets(sendBuf) != '\0')

    {

        if(send(clientsock, sendBuf, strlen(sendBuf) + sizeof(char), 0) == -1)

        {

            printf("send error!\n");

        }

        printf("send to server:%s\n", sendBuf);

        memset(sendBuf, 0, sizeof(sendBuf));



        if(recv(clientsock, recvBuf, MAX_NUM, 0) == -1)

        {

            printf("rev error!\n");

        }

        printf("receive from server:%s\n",recvBuf);

        if(strcmp(recvBuf,"Goodbye,my dear client!")==0)

        {

            break;

        }

        memset(recvBuf, 0, sizeof(recvBuf));

    }

    close(clientsock);

    return 0;

}

 

示例代码参考博客:https://www.cnblogs.com/gildoringlorin/p/3948809.html

 

基于UDP的C/S程序实例

     服务器端

       代码流程如下:

     1.创建服务器socket

     2.初始化端口和ip地址调用bind进行绑定

     3.调用recv和send与客户端进行通信

     4.调用close关闭网络环境和socket

     具体示例代码如下:

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<errno.h>

#include<sys/types.h>

#include<sys/socket.h>

#include<netinet/in.h>

#include<string.h>



#define MYPORT 8887





void echo_ser(int sock)

{

    char recvbuf[1024] = {0};

    struct sockaddr_in peeraddr;

    socklen_t peerlen;

    int n;

   

    while (1)

    {

       

        peerlen = sizeof(peeraddr);

        memset(recvbuf, 0, sizeof(recvbuf));

        n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0,

                     (struct sockaddr *)&peeraddr, &peerlen);

        if (n <= 0)

        {

           

            if (errno == EINTR)

                continue;

           

            ERR_EXIT("recvfrom error");

        }

        else if(n > 0)

        {

            printf("接收到的数据:%s\n",recvbuf);

            sendto(sock, recvbuf, n, 0,

                   (struct sockaddr *)&peeraddr, peerlen);

            printf("回送的数据:%s\n",recvbuf);

        }

    }

    close(sock);

}



int main(void)

{

    int sock;

    if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)

        printf("socket error");

   

    struct sockaddr_in servaddr;

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;

    servaddr.sin_port = htons(MYPORT);

    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

   

    printf("监听%d端口\n",MYPORT);

    if (bind(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)

        ERR_EXIT("bind error");

   

    echo_ser(sock);

    return 0;

}

     客户端

 

     代码流程如下:

     1.创建服务器socket

     2.调用recv和send与客户端进行通信

     3.调用close关闭网络环境和socket

     具体示例代码如下:

#include <unistd.h>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>

#include <stdlib.h>

#include <stdio.h>

#include <errno.h>

#include <string.h>



#define MYPORT 8887

char* SERVERIP = "127.0.0.1";



void echo_cli(int sock)

{

    struct sockaddr_in servaddr;

    memset(&servaddr, 0, sizeof(servaddr));

    servaddr.sin_family = AF_INET;

    servaddr.sin_port = htons(MYPORT);

    servaddr.sin_addr.s_addr = inet_addr(SERVERIP);

   

    int ret;

    char sendbuf[1024] = {0};

    char recvbuf[1024] = {0};

    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)

    {

       

        printf("向服务器发送:%s\n",sendbuf);

        sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr *)&servaddr, sizeof(servaddr));

       

        ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);

        if (ret == -1)

        {

            if (errno == EINTR)

                continue;

            ERR_EXIT("recvfrom");

        }

        printf("从服务器接收:%s\n",recvbuf);

       

        memset(sendbuf, 0, sizeof(sendbuf));

        memset(recvbuf, 0, sizeof(recvbuf));

    }

   

    close(sock);

   

   

}



int main(void)

{

    int sock;

    if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)

        printf("socket");



    echo_cli(sock);  

    return 0;

}

     参考博客:https://blog.csdn.net/lell3538/article/details/53335472

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
众所周知,人工智能是当前最热门的话题之一, 计算机技术与互联网技术的快速发展更是将对人工智能的研究推向一个新的高潮。 人工智能是研究模拟和扩展人类智能的理论与方法及其应用的一门新兴技术科学。 作为人工智能核心研究领域之一的机器学习, 其研究动机是为了使计算机系统具有人的学习能力以实现人工智能。 那么, 什么是机器学习呢? 机器学习 (Machine Learning) 是对研究问题进行模型假设,利用计算机从训练数据中学习得到模型参数,并最终对数据进行预测和分析的一门学科。 机器学习的用途 机器学习是一种通用的数据处理技术,其包含了大量的学习算法。不同的学习算法在不同的行业及应用中能够表现出不同的性能和优势。目前,机器学习已成功地应用于下列领域: 互联网领域----语音识别、搜索引擎、语言翻译、垃圾邮件过滤、自然语言处理等 生物领域----基因序列分析、DNA 序列预测、蛋白质结构预测等 自动化领域----人脸识别、无人驾驶技术、图像处理、信号处理等 金融领域----证券市场分析、信用卡欺诈检测等 医学领域----疾病鉴别/诊断、流行病爆发预测等 刑侦领域----潜在犯罪识别与预测、模拟人工智能侦探等 新闻领域----新闻推荐系统等 游戏领域----游戏战略规划等 从上述所列举的应用可知,机器学习正在成为各行各业都会经常使用到的分析工具,尤其是在各领域数据量爆炸的今天,各行业都希望通过数据处理与分析手段,得到数据中有价值的信息,以便明确客户的需求和指引企业的发展。
众所周知,人工智能是当前最热门的话题之一, 计算机技术与互联网技术的快速发展更是将对人工智能的研究推向一个新的高潮。 人工智能是研究模拟和扩展人类智能的理论与方法及其应用的一门新兴技术科学。 作为人工智能核心研究领域之一的机器学习, 其研究动机是为了使计算机系统具有人的学习能力以实现人工智能。 那么, 什么是机器学习呢? 机器学习 (Machine Learning) 是对研究问题进行模型假设,利用计算机从训练数据中学习得到模型参数,并最终对数据进行预测和分析的一门学科。 机器学习的用途 机器学习是一种通用的数据处理技术,其包含了大量的学习算法。不同的学习算法在不同的行业及应用中能够表现出不同的性能和优势。目前,机器学习已成功地应用于下列领域: 互联网领域----语音识别、搜索引擎、语言翻译、垃圾邮件过滤、自然语言处理等 生物领域----基因序列分析、DNA 序列预测、蛋白质结构预测等 自动化领域----人脸识别、无人驾驶技术、图像处理、信号处理等 金融领域----证券市场分析、信用卡欺诈检测等 医学领域----疾病鉴别/诊断、流行病爆发预测等 刑侦领域----潜在犯罪识别与预测、模拟人工智能侦探等 新闻领域----新闻推荐系统等 游戏领域----游戏战略规划等 从上述所列举的应用可知,机器学习正在成为各行各业都会经常使用到的分析工具,尤其是在各领域数据量爆炸的今天,各行业都希望通过数据处理与分析手段,得到数据中有价值的信息,以便明确客户的需求和指引企业的发展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值