Socket

Socket原理讲解

对TCP/IP、UDP、Socket编程这些词你不会很陌生吧?随着网络技术的发展,这些词充斥着我们的耳朵。那么我想问:

  1. 什么是TCP/IP、UDP?
  2. Socket在哪里呢?
  3. Socket是什么呢?
  4. 你会使用它们吗?

什么是TCP/IP、UDP?

​ TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。

​ UDP(User Data Protocol,用户数据报协议)是与TCP相对应的协议。它是属于TCP/IP协议族中的一种。
​ 这里有一张图,表明了这些协议的关系。

img

​ TCP/IP协议族包括运输层、网络层、链路层。现在你知道TCP/IP与UDP的关系了吧。

Socket在哪里呢?
在上图中,我们没有看到Socket的影子,那么它到底在哪里呢?还是用图来说话,一目了然。

img

原来Socket在这里。

Socket是什么呢?
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

你会使用它们吗?
前人已经给我们做了好多的事了,网络间的通信也就简单了许多,但毕竟还是有挺多工作要做的。以前听到Socket编程,觉得它是比较高深的编程知识,但是只要弄清Socket编程的工作原理,神秘的面纱也就揭开了。

​ 一个生活中的场景。你要打电话给一个朋友,先拨号,朋友听到电话铃声后提起电话,这时你和你的朋友就建立起了连接,就可以讲话了。等交流结束,挂断电话结束此次交谈。 生活中的场景就解释了这工作原理,也许TCP/IP协议族就是诞生于生活中,这也不一定。

img

​ 先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

============================================

​ 我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页 时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是 socket?socket的类型有哪些?还有socket的基本函数,这些都是本文想介绍的。本文的主要内容如下:

  • 1、网络中进程之间如何通信?
  • 2、Socket是什么?
  • 3、socket的基本操作
    • 3.1、socket()函数
    • 3.2、bind()函数
    • 3.3、listen()、connect()函数
    • 3.4、accept()函数
    • 3.5、read()、write()函数等
    • 3.6、close()函数
  • 4、socket中TCP的三次握手建立连接详解
  • 5、socket中TCP的四次握手释放连接详解
  • 6、一个例子

1、网络中进程之间如何通信?

要了解Socket首先要熟悉网络进程通信的原理,一个完整的网络应用程序包括客户端和服务器两个部分。网络间通信需要由两个进程组成,并且只能用同一种协议,也就是说,不能在通信的一端使用TCP协议,而另一端则用UDP协议。

在TCP/IP协议中,两个因特网主机通过两个路由器和对应的层连接。各主机上的应用通过一些数据通道相互执行读取操作,如下图所示:

图片

实现网络间通信,要解决一个首要问题是-如何唯一标识一个进程,在网络上,通常利用ip地址+协议+端口号唯一标示网络中的一个进程。IP层的ip地址可以唯一标示主机,而TCP层协议和端口号可以唯一标示主机的一个进程,这样它们就可以利用Socket进行通信了。

**本地的进程间通信(IPC)**有很多种方式,但可以总结为下面4类:

  • 消息传递(管道、FIFO、消息队列)
  • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
  • 共享内存(匿名的和具名的)
  • 远程过程调用(Solaris门和Sun RPC)

但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。

2、什么是Socket?

上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?Socket是一个进程间通信终结点,它是Socket应用程序用来在网络上发送或接收数据包的对象,是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。

如下图所示,在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

图片

Socket的使用类型主要有两种:

(1)流套接字(streamsocket):基于 TCP协议,采用流的方式提供可靠的字节流服务;

(2)数据报套接字(datagramsocket):基于 UDP协议,采用数据报文提供数据打包发送的服务。

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。

图片

1)服务器根据地址类型(ipv4,ipv6)、socket类型、协议创建socket;

(2)服务器为socket绑定ip地址和端口号;

(3)服务器socket监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket并没有被打开;

(4)客户端创建socket;

(5)客户端打开socket,根据服务器ip地址和端口号试图连接服务器socket;

(6)服务器socket接收到客户端socket请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求;

(7)客户端连接成功,向服务器发送连接状态信息;

(8)服务器accept方法返回,连接成功;

(9)客户端向socket写入信息;

(10)服务器读取信息;

(11)客户端关闭;

(12)服务器端关闭。

以上,一次socket通信流程的交互结束。

socket一词的起源

在组网领域的首次使用是在1970年2月12日发布的文献IETF RFC33中发现的,撰写者为Stephen Carr、Steve Crocker和Vint Cerf。根据美国计算机历史博物馆的记载,Croker写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”

3、socket的基本操作

既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。

1)创建一个socket:socket()
int socket(int domain, int type, int protocol);
 - domain:与socket通信的domain。
 - type:socket类型,SOCK_STREAM(TCP),SOCK_DGRAM(UDP)。
 - protocol:通常指定为0,在RAW_SOCKET中为IPPROTO_RAW。
return: 新创建socket的文件描述符。

(2)将socket绑定到地址:bind()
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
 - sockfd:在socket()调用取得的文件描述符。
 - addr:要socket绑定到的地址。
 - addrlen:制定了地址结构的大小。
 return-1为绑定失败。

(3)监听接入连接:listen()
int listen(int sockfd, int backlog);
 - sockfd:socket文件描述符。
 - backlog:限制未决连接的数量(在调用accept()前收到connect()的连接)。
return: -1为监听失败。

(4)接受连接:accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
 - sockfd:socket文件描述符。
 - addr:对端socket的地址结构。
 - addrlen:对端socket地址结构的长度。
return:和对端连接的文件描述符。
     当调用accept()时,会创建一个新的socket,并且由这个新创建的socket来与执行connect()的对等socket进行连接。

(5)连接到对等socket:connect()
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 - sockfd:socket文件描述符。
 - addr:要连接到socket的地址。
 - addrlen:地址结构的长度。

(6)连接终止:close()
    终止一个流socket的连接,如果多个文件描述符引用了一个socket,那么所有描述符被关闭后才会终止(若调用shutdown()则可以强制关闭socket上的信道)。

3.1、socket()函数

int socket(int domain, int type, int protocol);

调用该函数前,先要加载winsock,wsastartup().socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而**socket()**用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

  • domain:即协议域,又称为协议族(family)。

    常用的协议族有,AF_INET、AF_INET6、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的类型有哪些?)。

    类型 解释

    SOCK_STREAM 提供有序的、可靠的、双向的和基于连接的 字节流,使用 带外数据传送机制,为Internet地址族使用TCP。

    SOCK_DGRAM 支持无连接的、不可靠的和使用固定大小(通常很小)缓冲区的 数据报服务,为Internet地址族使用UDP。

    SOCK_STREAM类型的套接口为全双向的字节流。对于流类套接口,在接收或发送数据前必需处于已连接状态。用 connect()调用建立与另一套接口的连接,连接成功后,即可用 send()和recv()传送数据。当会话结束后,调用closesocket()。 带外数据根据规定用send()和recv()来接收。 原始套接字(SOCK_RAW):原始套接字与标准套接字(标准套接字指的是前面介绍的流套接字和数据包套接字)的 区别在于:原始套接字可以读写内核没有处理的IP数据包,而流套接字只能读取TCP协议的数据,数据包套接字只能读取UDP协议的数据。因此,如果要访问其他协议发送数据必须使用原始套接字。

    实现SOCK_STREAM类型 套接口的通讯协议保证数据不会丢失也不会重复。如果终端协议有缓冲区空间,且数据不能在一定时间成功发送,则认为连接中断,其后续的调用也将以WSAETIMEOUT错误返回。

    SOCK_DGRAM类型套接口允许使用 sendto()和recvfrom()从任意端口发送或接收数据报。如果这样一个套接口用 connect()与一个指定端口连接,则可用 send()和recv()与该端口进行数据报的发送与接收。

  • protocol:故名思意,就是指定协议。

    常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议(这个协议我将会单独开篇讨论!)。

    1 参数protocol用来指明所要接收的协议包,如果是象IPPROTO_TCP(6)这种非0、非255的协议,当操作系统内核碰到ip头中protocol域和创建socket所使用参数protocol相同的IP包,就会交给这个raw socket来处理,因此,一般来说,要想接收什么样的数据包,就应该在参数protocol里来指定相应的协议。当内核向此raw socket交付数据包的时候,是包括整个IP头的,并且已经是重组好的IP包。
    2 如果protocol是IPPROTO_RAW(255),这时候,这个socket只能用来发送IP包,而不能接收任何的数据。发送的数据需要自己填充IP包头,并且自己计算校验和。
    3 对于protocol为0(IPPROTO_IP)的raw socket。用于接收任何的IP数据包。其中的校验和和协议分析由程序自己完成。

返回值:

​ 如果成功就返回生成的SOCKET

​ 如果失败就返回INVALID_SOCKET(-1)

这是include/[Linux](http://lib.csdn.net/base/linux)/in.h里的定义:

/* Standard well-defined IP protocols. */
enum {
IPPROTO_IP = 0,  /* Dummy protocol for TCP  */
IPPROTO_ICMP = 1,  /* Internet Control Message Protocol */
IPPROTO_IGMP = 2,  /* Internet Group Management Protocol */
IPPROTO_IPIP = 4,  /* IPIP tunnels (older KA9Q tunnels use 94) */
IPPROTO_TCP = 6,  /* Transmission Control Protocol */
IPPROTO_EGP = 8,  /* Exterior Gateway Protocol  */
IPPROTO_PUP = 12,  /* PUP protocol   */
IPPROTO_UDP = 17,  /* User Datagram Protocol  */
IPPROTO_IDP = 22,  /* XNS IDP protocol  */
IPPROTO_DCCP = 33,  /* Datagram Congestion Control Protocol */
IPPROTO_RSVP = 46,  /* RSVP protocol  */
IPPROTO_GRE = 47,  /* Cisco GRE tunnels (rfc 1701,1702) */

IPPROTO_IPV6 = 41,  /* IPv6-in-IPv4 tunnelling  */

IPPROTO_ESP = 50,      /* Encapsulation Security Payload protocol */
IPPROTO_AH = 51,       /* Authentication Header protocol    */
IPPROTO_BEETPH = 94,    /* IP option pseudo header for BEET */
IPPROTO_PIM  = 103,  /* Protocol Independent Multicast */

IPPROTO_COMP  = 108,        /* Compression Header protocol */
IPPROTO_SCTP  = 132,  /* Stream Control Transport Protocol */
IPPROTO_UDPLITE = 136, /* UDP-Lite (RFC 3828)  */

IPPROTO_RAW = 255,  /* Raw IP packets  */
IPPROTO_MAX
};

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

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址,其实在调用socket函数创建socket时,内核还并未给socket分配源地址和源端口。而对于UDP,我猜测在调用sendto发送数据时,在未捆绑端口的情况下,内核也会随机分配端口。。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

3.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    108
    
    struct sockaddr_un { 
        sa_family_t sun_family;               /* AF_UNIX */ 
        char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
    };
    
  • addrlen:对应的是地址的长度。

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

在《UNIX网络编程》这本书中提到:“如果一个TCP客户或者服务器未曾调用bind捆绑一个端口,当调用connect或listen时,内核就要为相应的套接字选择一个临时接口。”从这句话中可以判断出,其实在调用socket函数创建socket时,内核还并未给socket分配源地址和源端口。而对于UDP,我猜测在调用sendto发送数据时,在未捆绑端口的情况下,内核也会随机分配端口。

而我遇到的特殊应用要求我在用UDP发送数据之前要告诉对方我的发送端口,这也就意味着我在sendto之前必须要捆绑端口,因此我在发送数据之前就得调用bind函数绑定一下端口了。但是我就在想内核既然有随机分配端口的能力,而我需要的也只是让它绑定一下而不用绑定在固定端口的业务,socket中应该能够提供这种业务。然后果然我发现bind就具备这种能力,当bind的参数中端口地址为0的时候,这时候就是由内核分配端口。这样我就不用考虑端口地址重复的问题,而放心的把这个问题交给内核处理了。

就在发现bind的这个机制的同时,我发现其实bind对于源地址也同样具备这种处理方式,当系统具有多IP(多网卡)的情况,当我们把bind函数中的ip参数置0时,就是由内核自己选择分配IP。而之前一直觉得很神奇的INADDR_ANY其实一点也不神奇,它的值其实就是0。所以当我们只有单一IP的时候,我们就可以用INADDR_ANY去代替那个单一的IP,因为内核分配的时候只能选择这一个IP。从而造成了INADDR_ANY就是本机IP的现象。

网络字节序与主机字节序

***主机字节序***就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

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

所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。

3.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服务器的连接。

3.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);

accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

3.5、read()、write()等函数

万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:

read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

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

       #include <unistd.h>

       ssize_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.h>

       ssize_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。

3.6、close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

#include <unistd.h>
int close(int fd);

close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

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

4、socket中TCP的三次挥手建立连接详解

我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

  • 客户端向服务器发送一个SYN J
  • 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
  • 客户端再想服务器发一个确认ACK K+1

SYN:同步序列编号 是TCP/IP建立连接时使用的握手信号

只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:

img

从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。

5、socket中TCP的四次握手释放连接详解

上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:

img

图示过程如下:

  • 某个应用进程首先调用 close主动关闭连接,这时TCP发送一个FIN M;
  • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
  • 一段时间之后,接收到文件结束符的应用进程调用 close关闭它的socket。这导致它的TCP也发送一个FIN N;
  • 接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

6、一个例子(实践一下)

说了这么多了,动手实践一下。下面编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。

服务器端代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define MAXLINE 4096

int main(int argc, char** argv)
{
    int    listenfd, connfd;
    struct sockaddr_in     servaddr;
    char    buff[4096];
    int     n;

    if( (listenfd = 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);
    servaddr.sin_port = htons(6666);

    if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){
    printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
    exit(0);
    }

    if( listen(listenfd, 10) == -1){
    printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
    exit(0);
    }

    printf("======waiting for client's request======\n");
    while(1){
    if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){
        printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
        continue;
    }
    n = recv(connfd, buff, MAXLINE, 0);
    buff[n] = '\0';
    printf("recv msg from client: %s\n", buff);
    close(connfd);
    }

    close(listenfd);
}

客户端代码:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>

#define MAXLINE 4096

int main(int argc, char** argv)
{
    int    sockfd, n;
    char    recvline[4096], sendline[4096];
    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(6666);
    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);
    }

    close(sockfd);
    exit(0);
}

当然上面的代码很简单,也有很多缺点,这就只是简单的演示socket的基本函数使用。其实不管有多复杂的网络程序,都使用的这些基本函数。上面的服务器使用的是迭代模式的,即只有处理完一个客户端请求才会去处理下一个客户端的请求,这样的服务器处理能力是很弱的,现实中的服务器都需要有并发处理能力!为了需要并发处理,服务器需要fork()一个新的进程或者线程去处理请求等。

7、面试题

1、LINUX下的Socket套接字和Windows下的WinSock有什么共同点?请从C/C++语言开发的角度描述,至少说出两点共同点。(10分,说得好的每点加5分,没有上限。精通SOCK编程者破格录用。)

本题的目的就是考考应试者的SOCK功底,以及应用能力。答案可谓是五花八门,但是答对的少得可怜。其实答案很多,也很好回答。可见面试者功底确实一般,对SOCK编程只是一知半解。更有甚者,把TCP/IP的原理搬出来讲了一大堆,却无一条符合要求。

参考答案:
第1题,答中一个得5分,答出其它正确答案的,也得5分。
a)都基于TCP/IP协议,都提供了面向连接的TCP SOCK和无连接的UDP SOCK。
b)都是一个sock结构体。
c)都是使用sock文件句柄进行访问。
d)都具有缓冲机制。

前言

Socket在实际系统程序开发当中,应用非常广泛,也非常重要。实际应用中服务器经常需要支持多个客户端连接,实现高并发服务器模型显得尤为重要。高并发服务器从简单的循环服务器模型处理少量网络并发请求,演进到解决C10K,C10M问题的高并发服务器模型。本文通过一个简单的多线程模型,带领大家学习如何自己实现一个简单的并发服务器。

C/S架构

服务器-客户机,即Client-Server(C/S)结构。C/S结构通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。

在C/S结构中,应用程序分为两部分:服务器部分和客户机部分。服务器部分是多个用户共享的信息与功能,执行后台服务,如控制共享数据库的操作等;客户机部分为用户所专有,负责执行前台功能,在出错提示、在线帮助等方面都有强大的功能,并且可以在子程序间自由切换。

图片

如上图所示:这是基于套接字实现客户端和服务器相连的函数调用关系,socket API资料比较多,本文不再过多叙述。

pthread线程库:(POSIX)

pthread线程库是Linux下比较常用的一个线程库,关于他的用法和特性大家可以自行搜索相关文章,下面只简单介绍他的用法和编译。

线程标识

线程有ID, 但不是系统唯一, 而是进程环境中唯一有效. 线程的句柄是pthread_t类型, 该类型不能作为整数处理, 而是一个结构. 下面介绍两个函数:

头文件: <pthread.h>
原型: int pthread_equal(pthread_t tid1, pthread_t tid2);
返回值: 相等返回非0, 不相等返回0.
说明: 比较两个线程ID是否相等.

头文件: <pthread.h>
原型: pthread_t pthread_self();
返回值: 返回调用线程的线程ID.

线程创建

在执行中创建一个线程, 可以为该线程分配它需要做的工作(线程执行函数), 该线程共享进程的资源. 创建线程的函数pthread_create()

头文件: <pthread.h>
原型: int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(start_rtn)(void), void *restrict arg);
返回值: 成功则返回0, 否则返回错误编号.
参数:
tidp: 指向新创建线程ID的变量, 作为函数的输出.
attr: 用于定制各种不同的线程属性, NULL为默认属性(见下).
start_rtn: 函数指针, 为线程开始执行的函数名.该函数可以返回一个void *类型的返回值,
而这个返回值也可以是其他类型,并由 pthread_join()获取
arg: 函数的唯一无类型(void)指针参数, 如要传多个参数, 可以用结构封装.

编译

因为pthread的库不是linux系统的库,所以在进行编译的时候要加上     -lpthread
# gcc filename -lpthread  //默认情况下gcc使用c库,要使用额外的库要这样选择使用的库

常见的网络服务器模型

本文结合自己的理解,主要以TCP为例,总结了几种常见的网络服务器模型的实现方式,并最终实现一个简单的命令行聊天室。

单进程循环

单线进程循环原理就是主进程没和客户端通信,客户端都要先连接服务器,服务器接受一个客户端连接后从客户端读取数据,然后处理并将处理的结果返还给客户端,然后再接受下一个客户端的连接请求。

优点单线程循环模型优点是简单、易于实现,没有同步、加锁这些麻烦事,也没有这些开销。

缺点

  1. 阻塞模型,网络请求串行处理;
  2. 没有利用多核cpu的优势,网络请求串行处理;
  3. 无法支持同时多个客户端连接;
  4. 程序串行操作,服务器无法实现同时收发数据。图片

单线程IO复用

linux高并发服务器中常用epoll作为IO复用机制。线程将需要处理的socket读写事件都注册到epoll中,当有网络IO发生时,epoll_wait返回,线程检查并处理到来socket上的请求。

优点

  1. 实现简单, 减少锁开销,减少线程切换开销。

缺点

  1. 只能使用单核cpu,handle时间过长会导致整个服务挂死;
  2. 当有客户端数量超过一定数量后,性能会显著下降;
  3. 只适用高IO、低计算,handle处理时间短的场景。

图片

多线程/多进程

多线程、多进程模型主要特点是每个网络请求由一个进程/线程处理,线程内部使用阻塞式系统调用,在线程的职能划分上,可以由一个单独的线程处理accept连接,其余线程处理具体的网络请求(收包,处理,发包);还可以多个进程单独listen、accept网络连接。

优点:

1、实现相对简单;2、利用到CPU多核资源。

缺点:

1、线程内部还是阻塞的,举个极端的例子,如果一个线程在handle的业务逻辑中sleep了,这个线程也就挂住了。图片

多线程/多进程IO复用

多线程、多进程IO服用模型,每个子进程都监听服务,并且都使用epoll机制来处理进程的网络请求,子进程 accept() 后将创建已连接描述符,然后通过已连接描述符来与客户端通信。该机制适用于高并发的场景。

优点:

  1. 支撑较高并发。

缺点:

  1. 异步编程不直观、容易出错

图片

多线程划分IO角色

多线程划分IO角色主要功能有:一个accept thread处理新连接建立;一个IO thread pool处理网络IO;一个handle thread pool处理业务逻辑。使用场景如:电销应用,thrift TThreadedSelectorServer。

优点:

  1. 按不同功能划分线程,各线程处理固定功能,效率更高
  2. 可以根据业务特点配置线程数量来性能调优

缺点:

  1. 线程间通信需要引入锁开销
  2. 逻辑较复杂,实现难度大

图片

小结

上面介绍了常见的网络服务器模型,还有AIO、协程,甚至还有其他的变型,在这里不再讨论。重要的是理解每种场景中所面临的问题和每种模型的特点,设计出符合应用场景的方案才是好方案。

多线程并发服务器模型

下面我们主要讨论多线程并发服务器模型。

代码结构

并发服务器代码结构如下:

thread_func()
{
  while(1) {
    recv(...);
    process(...);
    send(...);
  }
  close(...);
}
main(
 socket(...); 
 bind(...);
 listen(...);
 while(1) { 
  accept(...);
  pthread_create();
 }
}

由上可以看出,服务器分为两部分:主线程、子线程。

主线程

main函数即主线程,它的主要任务如下:

  1. socket()创建监听套字;
  2. bind()绑定端口号和地址;
  3. listen()开启监听;
  4. accept()等待客户端的连接,
  5. 当有客户端连接时,accept()会创建一个新的套接字new_fd;
  6. 主线程会创建子线程,并将new_fd传递给子线程。

子线程

  1. 子线程函数为thread_func(),他通过new_fd处理和客户端所有的通信任务。

客户端连接服务器详细步骤

下面我们分步骤来看客户端连接服务器的分步说明。

1. 客户端连接服务器

  1. 服务器建立起监听套接字listen_fd,并初始化;
  2. 客户端创建套接字fd1;
  3. 客户端client1通过套接字fd1连接服务器的listen_fd;

图片

2. 主线程创建子线程thread1

  1. server收到client1的连接请求后,accpet函数会返回一个新的套接字newfd1;
  2. 后面server与client1的通信就依赖newfd1,监听套接字listen_fd会继续监听其他客户端的连接;
  3. 主线程通过pthead_create()创建一个子线程thread1,并把newfd1传递给thread1;
  4. server与client1的通信就分别依赖newfd1、fd1。
  5. client1为了能够实时收到server发送的信息,同时还要能够从键盘上读取数据,这两个操作都是阻塞的,没有数据的时候进程会休眠,所以必须创建子线程read_thread;
  6. client1的主线负责从键盘上读取数据并发送给,子线程read_thread负责从server接受信息。

图片

3. client2连接服务器

  1. 客户端client2创建套接字fd2;
  2. 通过connect函数连接server的listen_fd;图片

4. 主线程创建子线程thread2

  1. server收到client2的连接请求后,accpet函数会返回一个新的套接字newfd2;
  2. 后面server与client2的通信就依赖newfd2,监听套接字listen_fd会继续监听其他客户端的连接;
  3. 主线程通过pthead_create()创建一个子线程thread2,并把newfd2传递给thread2;
  4. server与client1的通信就分别依赖newfd2、fd2。
  5. 同样client2为了能够实时收到server发送的信息,同时还要能够从键盘上读取数据必须创建子线程read_thread;
  6. client1的主线负责从键盘上读取数据并发送给,子线程read_thread负责从server接受信息。

图片

由上图可见,每一个客户端连接server后,server都要创建一个专门的thread负责和该客户端的通信;每一个客户端和server都有一对固定的fd组合用于连接。

实例

好了,理论讲完了,根据一口君的惯例,也继承祖师爷的教诲:talk is cheap,show you my code.不上代码,只写理论的文章都是在耍流氓。

本例的主要功能描述如下:

  1. 实现多个客户端可以同时连接服务器;
  2. 客户端可以实现独立的收发数据;
  3. 客户端发送数据给服务器后,服务器会将数据原封不动返回给客户端。
服务器端
/*********************************************
           服务器程序  TCPServer.c  
           公众号:一口Linux
*********************************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>

#define RECVBUFSIZE 2048
void *rec_func(void *arg)
{
 int sockfd,new_fd,nbytes;
 char buffer[RECVBUFSIZE];
 int i;
 new_fd = *((int *) arg);
 free(arg); 
 
 while(1)
 {
  if((nbytes=recv(new_fd,buffer, RECVBUFSIZE,0))==-1)
  {
   fprintf(stderr,"Read Error:%s\n",strerror(errno));
   exit(1);
  }
  if(nbytes == -1)
  {//客户端出错了 返回值-1
   close(new_fd);
   break;   
  }
  if(nbytes == 0)
  {//客户端主动断开连接,返回值是0
   close(new_fd);
   break;
  }
  buffer[nbytes]='\0'; 
  printf("I have received:%s\n",buffer); 
  
  
  if(send(new_fd,buffer,strlen(buffer),0)==-1)
  {
   fprintf(stderr,"Write Error:%s\n",strerror(errno));
   exit(1);
  }
   
 }

}

int main(int argc, char *argv[])
{
 char buffer[RECVBUFSIZE];
 int sockfd,new_fd,nbytes;
 struct sockaddr_in server_addr;
 struct sockaddr_in client_addr;
 int sin_size,portnumber;
 char hello[]="Hello! Socket communication world!\n";
 pthread_t tid;
 int *pconnsocke = NULL;
 int ret,i;
 
 if(argc!=2)
 {
  fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);
  exit(1);
 }
 /*端口号不对,退出*/
 if((portnumber=atoi(argv[1]))<0)
 {
  fprintf(stderr,"Usage:%s portnumber\a\n",argv[0]);
  exit(1);
 }

 /*服务器端开始建立socket描述符  sockfd用于监听*/
 if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)  
 {
  fprintf(stderr,"Socket error:%s\n\a",strerror(errno));
  exit(1);
 }
 
 /*服务器端填充 sockaddr结构*/ 
 bzero(&server_addr,sizeof(struct sockaddr_in));
 server_addr.sin_family     =AF_INET;
 /*自动填充主机IP*/
 server_addr.sin_addr.s_addr=htonl(INADDR_ANY);//自动获取网卡地址
 server_addr.sin_port       =htons(portnumber);
 
 /*捆绑sockfd描述符*/ 
 if(bind(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
 {
  fprintf(stderr,"Bind error:%s\n\a",strerror(errno));
  exit(1);
 }
 
 /*监听sockfd描述符*/
 if(listen(sockfd, 10)==-1)
 {
  fprintf(stderr,"Listen error:%s\n\a",strerror(errno));
  exit(1);
 }

 while(1)
 {
  /*服务器阻塞,直到客户程序建立连接*/
  sin_size=sizeof(struct sockaddr_in);
  if((new_fd = accept(sockfd,(struct sockaddr *)&client_addr,&sin_size))==-1)
  {
   fprintf(stderr,"Accept error:%s\n\a",strerror(errno));
   exit(1);
  }
  
  pconnsocke = (int *) malloc(sizeof(int));
  *pconnsocke = new_fd;
  
  ret = pthread_create(&tid, NULL, rec_func, (void *) pconnsocke);
  if (ret < 0) 
  {
   perror("pthread_create err");
   return -1;
  } 
 }
 //close(sockfd);
 exit(0);
}

客户端
/*********************************************
           服务器程序  TCPServer.c  
           公众号:一口Linux
*********************************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#define RECVBUFSIZE 1024

void *func(void *arg)
{
 int sockfd,new_fd,nbytes;
 char buffer[RECVBUFSIZE];
 
 new_fd = *((int *) arg);
 free(arg);
 
 while(1)
 {
  if((nbytes=recv(new_fd,buffer, RECVBUFSIZE,0))==-1)
  {
   fprintf(stderr,"Read Error:%s\n",strerror(errno));
   exit(1);
  }
  buffer[nbytes]='\0';
  printf("I have received:%s\n",buffer); 
 }

}

int main(int argc, char *argv[])
{
 int sockfd;
 char buffer[RECVBUFSIZE];
 struct sockaddr_in server_addr;
 struct hostent *host;
 int portnumber,nbytes; 
 pthread_t tid;
 int *pconnsocke = NULL;
 int ret;
 
 //检测参数个数
 if(argc!=3)
 {
  fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
  exit(1);
 }
 //argv2 存放的是端口号 ,读取该端口,转换成整型变量
 if((portnumber=atoi(argv[2]))<0)
 {
  fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);
  exit(1);
 }
 //创建一个 套接子
 if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)
 {
  fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));
  exit(1);
 }

 //填充结构体,ip和port必须是服务器的
 bzero(&server_addr,sizeof(server_addr));
 server_addr.sin_family=AF_INET;
 server_addr.sin_port=htons(portnumber);
 server_addr.sin_addr.s_addr = inet_addr(argv[1]);//argv【1】 是server ip地址

 /*¿Í»§³ÌÐò·¢ÆðÁ¬œÓÇëÇó*/ 
 if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr))==-1)
 {
  fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));
  exit(1);
 }
 
 //创建线程
 pconnsocke = (int *) malloc(sizeof(int));
 *pconnsocke = sockfd;
 
 ret = pthread_create(&tid, NULL, func, (void *) pconnsocke);
 if (ret < 0) 
 {
  perror("pthread_create err");
  return -1;
 } 
 while(1)
 {
 #if 1
  printf("input msg:");
  scanf("%s",buffer);
  if(send(sockfd,buffer,strlen(buffer),0)==-1)
  {
   fprintf(stderr,"Write Error:%s\n",strerror(errno));
   exit(1);
  }
  #endif
 }
 close(sockfd);
 exit(0);
}

编译编译线程,需要用到pthread库,编译命令如下:

  1. gcc s.c -o s -lpthread
  2. gcc cli.c -o c -lpthread 先本机测试
  3. 开启一个终端 ./s 8888
  4. 再开一个终端 ./cl 127.0.0.1 8888,输入一个字符串"qqqqqqq"
  5. 再开一个终端 ./cl 127.0.0.1 8888,输入一个字符串"yikoulinux"

图片

有读者可能会注意到,server创建子线程的时候用的是以下代码:

 pconnsocke = (int *) malloc(sizeof(int));
  *pconnsocke = new_fd;
  
  ret = pthread_create(&tid, NULL, rec_func, (void *) pconnsocke);
  if (ret < 0) 
  {
   perror("pthread_create err");
   return -1;
  } 

为什么必须要malloc一块内存专门存放这个新的套接字呢?

:%s\a\n",strerror(errno));
exit(1);
}

//创建线程
pconnsocke = (int *) malloc(sizeof(int));
*pconnsocke = sockfd;

ret = pthread_create(&tid, NULL, func, (void *) pconnsocke);
if (ret < 0)
{
perror(“pthread_create err”);
return -1;
}
while(1)
{
#if 1
printf(“input msg:”);
scanf("%s",buffer);
if(send(sockfd,buffer,strlen(buffer),0)==-1)
{
fprintf(stderr,“Write Error:%s\n”,strerror(errno));
exit(1);
}
#endif
}
close(sockfd);
exit(0);
}


**编译**编译线程,需要用到pthread库,编译命令如下:

1. gcc s.c -o s -lpthread
2. gcc cli.c -o c -lpthread 先本机测试
3. 开启一个终端 ./s 8888
4. 再开一个终端 ./cl 127.0.0.1 8888,输入一个字符串"qqqqqqq"
5. 再开一个终端 ./cl 127.0.0.1 8888,输入一个字符串"yikoulinux"

[外链图片转存中...(img-IBVKFAF4-1626881031420)]

有读者可能会注意到,server创建子线程的时候用的是以下代码:

```c
 pconnsocke = (int *) malloc(sizeof(int));
  *pconnsocke = new_fd;
  
  ret = pthread_create(&tid, NULL, rec_func, (void *) pconnsocke);
  if (ret < 0) 
  {
   perror("pthread_create err");
   return -1;
  } 

为什么必须要malloc一块内存专门存放这个新的套接字呢?

这个是一个很隐蔽,很多新手都容易犯的错误。下一章,我会专门给大家讲解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南 城

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值