笔记2:VC++ socket通信实例

VC++ socket通信实例

网络中进程之间如何通信

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

Socket是什么

  socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
说白了Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

名词解释

网络协议

  为了进行网络中的数据交换(通信)而建立的规则、标准或约定;
  协议 = 语义 + 语法 + 规则
  不同层具有各自不同的协议,上层使用下层提供的服务,实际通信在最底层完成;对等层实体之间虚拟通信。

TCP协议:

  面向连接的可靠的传输协议;

UDP协议:

  面向无连接的不可靠的传输协议;

端口

  一种抽象的软件结构。
  应用程序通过系统调用与某端口建立连接后,传输层传给端口的数据都被响应的进行所接收,相应进程发给传输层的数据都通过该端口输出;
  端口用一个整数型标示符来表示。端口号跟协议相关,TCP/IP传输层的两个协议TCP和UDP是完全独立的两个软件模块,因此各自的端口号也相互独立
  端口使用一个16位的数字来表示,它的范围是0~65535, 1024以下的端口号保留给预定义的服务。例如:http使用的是80端口。

socket(套接字)

  windows sockets只支持一个通信区域:网际域(AF_INET),这个域被使用网际协议簇通信的进程使用。

C/S 模式

  客户机向服务器提出请求,服务器接收到请求后,提供相应的服务。网络通讯的标准模式;

socket(套接字的模式)

  • 流式套接字(SOCK_STREAM):面向连接可靠的数据传输服务,按顺序接收。例如:TCP/IP
  • 数据包式套接字(SOCK_DGRAM):无连接服务。接收顺序混乱。例如:UDP
  • 原始套接字(SOCK_RAM

基于TCP(面向连接可靠)的socket编程

序号服务器客户端
1创建socket创建socket
2绑定IP和端口绑定IP和端口
3设置Socket为监听模式
4收到客户Connect请求,返回一个新的对应于此次连接的Socket Accept向服务区发起connect请求
5与客户端进行通信,发送指令或数据与服务器进行通讯,send/recv
6等待其他客户的请求…
7关闭Socket关闭Socket

在这里插入图片描述

Socket的基本操作

socket 所需的头文件

#include <winsock2.h>

socket 套接字

int socket(
nt domain,				// 协议域 
int type, 				// socket类型
int protocol);			// 协议
);

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

  正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:
domain:即协议域,又称为协议族(family)
  常用的协议族有,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等等

protocol:就是指定协议
  常用的协议有,IPPROTO_TCP、PPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

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

命名socket

  SOCK_STREAM式套接字的通信双方均需要具有地址,其中服务器端的地址需要明确指定,ipv4的指定方法是使用 struct sockaddr_in类型的变量。

// 演示
struct sockaddr_in     servaddr;  
memset(&servaddr, 0, sizeof(servaddr));    	// 初始化  
servaddr.sin_family = AF_INET;  			// 协议
// 记住 htonl(INADDR_ANY) 获取本机IP地址
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。  
// 记住 htons(DEFAULT_PORT)
servaddr.sin_port = htons(DEFAULT_PORT);    //设置的端口

  INADDR_ANY 就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。也就是表示本机的所有IP,因为有些机子不止一块网卡,多网卡的情况下,这个就表示所有网卡ip地址的意思。

注意
客户端connect时,不能使用 INADDR_ANY选项。必须指明要连接哪个服务器IP。

htons函数 将主机的无符号短整形数转换 成网络字节顺序
htonl函数 将主机的无符号长整形数转换 成网络字节顺序

扩展:
网络字节序与主机字节序:
  主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。
引用标准的Big-Endian和Little-Endian的定义如下:
  a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
  b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。
  网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序(Big-Endian)。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,当然一个字节的数据没有顺序的问题了。

关键点
  在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。
谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket

  当我们调用socket()创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数(服务器端),否则就当调用connect()、listen()时系统会自动随机分配一个端口(客户端)。

bind()函数

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

int bind(
	int sockfd, 					// socket描述字
	const struct sockaddr *addr,   	// 地址 
	socklen_t addrlen);				// 结构长度

函数的三个参数分别为:
sockfd
  即socket描述字(SOCKADDR_IN 类型变量),它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字

addr
  一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。

struct sockaddr{
    sa_family_t  sin_family;   // 地址族(Address Family),也就是地址类型
    char         sa_data[14];  // IP地址和端口号 比如:127.0.0.1:80
};

一般采用SOCKADDR_IN 结构变量

	SOCKADDR_IN addrSock;
	addrSock.sin_family = AF_INET;
	addrSock.sin_port = htons(3030);					// 固定监听端口  3030 
	addrSock.sin_addr.s_addr = htonl(INADDR_ANY);		// INADDR_ANY 表示任何IP  即:本机的全部网口IP

addrlen:对应的是地址的长度。

说明:
  sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,但没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值。正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。

如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 */ 
};

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

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

accept()函数 (服务器端)

TCP模式下,客户端与服务器端的握手流程

  1. TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。
  2. TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。
  3. TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
int accept(
	int sockfd, 			// 服务器的socket描述字,称为监听描述字
	struct sockaddr *addr,  // struct sockaddr *,返回客户端的协议地址
	socklen_t *addrlen);	//  返回客户端的协议地址结构的长度
	
// 注意:返回值为全新的描述字,表示客户的TCP连接

accept函数的第一个参数为服务器的socket描述字,
第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,
第三个参数为客户端协议地址的长度。

如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接

注意
   accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字

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

问题:客户端异常关闭,为什么服务器端程序会崩溃?

解答:

recv()、send()等几组函数

   至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信。

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

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

它们的声明如下:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);				// fd 文件描述符,即socket描述字
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() and write()

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

  write函数将buf中的nbytes字节内容写入文件描述符fd。
成功时返回写的字节数。失败时返回-1,并设置errno变量
  在网络程序中,当我们向套接字文件描述符写时有两种可能。

  1. write的返回值大于0,表示写了部分或者是全部的数据。
  2. 返回的值小于0,此时出现了错误。

我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

send() and recv()

   recv函数和send函数提供了read和write函数一样的功能,不同的是他们提供了四个参数。
   前面的三个参数和read、write函数是一样的。第四个参数可以是0或者是以下组合:

MSG_DONTROUTE:不查找表,是send函数使用的标志,这个标志告诉IP,目的主机在本地网络上,没有必要查找表,这个标志一般用在网络诊断和路由程序里面。

MSG_OOB:表示可以接收和发送带外数据

MSG_PEEK:查看数据,并不从系统缓冲区移走数据。是recv函数使用的标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容。这样在下次读取的时候,依然是一样的内容,一般在有多个进程读写数据的时候使用这个标志。

MSG_WAITALL等待所有数据,是recv函数的使用标志,表示等到所有的信息到达时才返回,使用这个标志的时候,recv返回一直阻塞,直到指定的条件满足时,或者是发生了错误。

  同步 Socket 的send函数的执行流程。当调用该函数时,

  1. send()函数先比较待发送数据的长度len和套接字s的发送缓冲的长度, 如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR
  2. 如果len小于或者等于s的发送缓冲区的长度,那么send先检查协议s的发送缓冲中的数据是否正在发送,如果是就等待协议把数据发送完;如果协议还没有开始发送s的发送缓冲中的数据或者s的发送缓冲中没有数据,那么send就比较s的发送缓冲区的剩余空间和len;
  3. 如果len大于剩余空间大小,send就一直等待协议把s的发送缓冲中的数据发送完 ;
  4. 如果len小于剩余 空间大小,send就仅仅把buf中的数据copy到剩余空间里(注意:并不是send把s的发送缓冲中的数据传到连接的另一端的,而是协议传的,send仅仅是把buf中的数据copy到s的发送缓冲区的剩余空间里)。

   如果send函数copy数据成功,就返回实际copy的字节数,如果send在copy数据时出现错误,那么send就返回SOCKET_ERROR;如果send在等待协议传送数据时网络断开的话,那么send函数也返回SOCKET_ERROR。

注意
   send函数把buf中的数据成功copy到s的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个socket函数就会返回SOCKET_ERROR。
(每一个除send外的socket函数在执 行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该Socket函数就返回 SOCKET_ERROR)

注意
在Unix系统下,如果send在等待协议传送数据时网络断开的话,调用send的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

  通过测试发现,异步socket的send函数在网络刚刚断开时还能发送返回相应的字节数,同时使用select检测也是可写的,但是过几秒钟之后,再send就会出错了,返回-1。select也不能检测出可写了。

  同步Socket的recv函数的执行流程。当应用程序调用recv函数时,

  1. recv先等待s的发送缓冲中的数据被协议传送完毕,如果协议在传送s的发送缓冲中的数据时出现网络错误,那么recv函数返回SOCKET_ERROR,
  2. 如果s的发送缓冲中没有数据或者数据被协议成功发送完毕后,recv先检查套接字s的接收缓冲区,如果s接收缓冲区中没有数据或者协议正在接收数据,那么recv就一直等待,直到协议把数据接收完毕。当协议把数据接收完毕,recv函数就把s的接收缓冲中的数据copy到buf中(注意协议接收到的数据可能大于buf的长度,所以 在这种情况下要调用几次recv函数才能把s的接收缓冲中的数据copy完。recv函数仅仅是copy数据,真正的接收数据是协议来完成的),recv函数返回其实际copy的字节数。如果recv在copy时出错,那么它返回SOCKET_ERROR;如果recv函数在等待协议接收数据时网络中断了,那么它返回0。

注意:
在Unix系统下,如果recv函数在等待协议接收数据时网络断开了,那么调用recv的进程会接收到一个SIGPIPE信号,进程对该信号的默认处理是进程终止。

select()函数

   connect、accept、recv或recvfrom这些是阻塞程序
(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。

   Select可以完成非阻塞方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常
(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)。

int select(
	int maxfdp,
	fd_set *readfds,
	fd_set *writefds,
	fd_set *errorfds,
	struct timeval*timeout); 

   struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(filedescriptor),即文件句柄,fd_set集合可以通过一些宏由人为来操作。

FD_ZERO(fd_set *set);           //Clear all entries from the set.  
FD_SET(int fd, fd_set *set);    //Add fd to the set.  
FD_CLR(int fd, fd_set *set);    //Remove fd from the set.  
FD_ISSET(int fd, fd_set *set);  //Return true if fd is in the set. 

struct timeval代表时间值。

struct timeval {  
int tv_sec;     //seconds  
int tv_usec;    //microseconds,注意这里是微秒不是毫秒  
}; 
  1. int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1

  2. fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select()就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值
    可以传入NULL值,表示不关心任何文件的读变化。

  3. fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

  4. fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常

  5. struct timeval *timeout是select()的超时时间,这个参数至关重要,它可以使select处于三种状态,
    第一,若将NULL以形参传入,即不传入时间结构,就是将select()置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
    第二,若将时间值设为0秒0微秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
    第三,timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值
   返回状态发生变化的描述符总数。 负值:select错误 ;正值:某些文件可读写或出错 ;0:等待超时,没有可读写或错误的文件

理解select模型:
   理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
(1)执行fd_set set; FD_ZERO(&set); 则set用位表示是0000,0000。
(2)若fd=5, 执行 FD_SET(fd,&set); 后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行**select(6,&set,0,NULL,NULL)**阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

基于上面的讨论,可以轻松得出select模型的特点:

(1)可监控的文件描述符个数取决与**sizeof(fd_set)**的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。

(2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和 fd_set 进行 FD_ISSET 判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

(3)可见select模型必须在select前循环array(加fd,取maxfd),select返回后循环array(FD_ISSET判断是否有时间发生)。
使用select和non-blocking实现server处理多client实例
在这里插入图片描述
点击下面网址继续记录笔记
[https://www.cnblogs.com/straight/articles/7660889.html]

close()/shutdown()函数

int close(int sockfd);  

   close(即:关闭)一个套接字的默认行为是把套接字标记为已关闭,然后立即返回到调用进程,该套接字描述符不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数,然而 TCP将尝试发送已排队等待发送到对端,发送完毕后发生的是正常的TCP连接终止序列。

  在多进程并发服务器中,父子进程共享着套接字,套接字描述符引用计数记录着共享着的进程个数,当父进程或某一子进程close掉套接字时,描述符引用计数会相应的减一,当引用计数仍大于零时,这个close调用就不会引发TCP的四路握手断连过程。

int shutdown(int sockfd,int howto);  

该函数的行为依赖于howto的值
SHUT_RD:值为0,关闭连接的读这一半。
SHUT_WR:值为1,关闭连接的写这一半。
SHUT_RDWR:值为2,连接的读和写都关闭。

   终止网络连接的通用方法是调用close函数,但使用shutdown能更好的控制断连过程(使用第二个参数)。

close与shutdown的区别主要表现在:
   close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,并且有时候这是非常重要的 ,特别是对于多进程并发服务器来说。
  而shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式。

socket中TCP的三次握手建立连接详解

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

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

   这样就完了三次握手,但是这个三次握手发生在socket的哪几个函数中:
在这里插入图片描述
   从图中可以看出,当客户端调用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在三次握手的第三次返回。

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

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

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

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

Client 端代码

#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>

#define SERV_TCP_PORT 8000 /* server's port */

int main(int argc, char *argv[])
{
  int sockfd;
  struct sockaddr_in serv_addr;         	// ipv4 地址
  char *serv_host = "localhost";			// 本地服务器 即:本机
  struct hostent *host_ptr;
  int port;
  int buff_size = 0;

  /* command line: client [host [port]]*/
  if(argc >= 2) 
     serv_host = argv[1]; 					/* read the host if provided */

  if(argc == 3)
     sscanf(argv[2], "%d", &port); 		/* read the port if provided */
  else 
     port = SERV_TCP_PORT;

  /* get the address of the host */
  if((host_ptr = gethostbyname(serv_host)) == NULL) {        // gethostbyname()
     perror("gethostbyname error");
     exit(1);
  }

  if(host_ptr->h_addrtype !=  AF_INET) {							// ?? 协议
     perror("unknown address type");
     exit(1);
  }

  bzero((char *) &serv_addr, sizeof(serv_addr));					//bzero
  serv_addr.sin_family = AF_INET;
  serv_addr.sin_addr.s_addr = 
     ((struct in_addr *)host_ptr->h_addr_list[0])->s_addr;
  serv_addr.sin_port = htons(port);


  /* open a TCP socket */
  // 返回值??
  if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
     perror("can't open stream socket");
     exit(1);
  }

  /* connect to the server */    
  //  必要的格式转换(struct sockaddr *)  将 sockaddr_in指针 转为 sockaddr指针
  // 返回值??
  if(connect(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
     perror("can't connect to server");
     exit(1);
  }

  /* write a message to the server */
  write(sockfd, "hello world", sizeof("hello world"));

  close(sockfd);
}

Service 端代码

#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

#define SERV_TCP_PORT 8000 /* server's port number */
#define MAX_SIZE 80

int main(int argc, char *argv[])
{
  int sockfd, newsockfd, clilen;
  struct sockaddr_in cli_addr, serv_addr;
  int port;
  char string[MAX_SIZE];
  int len;

  /* command line: server [port_number] */

  if(argc == 2) 
     sscanf(argv[1], "%d", &port); /* read the port number if provided */
  else
     port = SERV_TCP_PORT;

  /* open a TCP socket (an Internet stream socket) */
  if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
     perror("can't open stream socket");
     exit(1);
  }

  /* bind the local address, so that the cliend can send to server */
  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 = htons(port);

  if(bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
     perror("can't bind local address");
     exit(1);
  }

  /* listen to the socket */
  listen(sockfd, 5);		// 第二个参数5,表示允许连接的客户端数量

  for(;;) {

     /* wait for a connection from a client; this is an iterative server */
     clilen = sizeof(cli_addr);
     newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);

     if(newsockfd < 0) {
        perror("can't bind local address");
     }

     /* read a message from the client */
     len = read(newsockfd, string, MAX_SIZE); 
     /* make sure it's a proper string */
     string[len] = 0;
     printf("%s\n", string);

	// 与客户端的连接socket用完关闭
     close(newsockfd);
  }  
}

基于UDP(无连接不可靠)的socket编程

序号服务器 (接收端)客户端(发送端)
1创建socket创建socket
2绑定IP和端口
3等待接收数据 recv/from向服务器发送数据 sendto
4关闭Socket关闭Socket

说明:

  本程序是基于windows socket的套接字库实现网络编程;
  当WSAStartup函数调用成功后,在程序的最后都需要相应的调用WSACleanup函数以便释放为该应用程序分配的资源,终止对WinSock动态库的使用。

  在基于UDP的套接字编程来说,我们把先启动的一端称为接收端,即服务端。主动先发送数据的一端称为发送端,也就是客户端。

基于UDP的服务器端流程

  1. 创建套接字(socket)
  2. 将套接字和IP地址、端口号绑定在一起(bind)
  3. 等待客户端发起数据通信(recvfrom/sendto)
  4. 关闭套接字

基于UDP的客户端流程

  1. 创建套接字(socket)
  2. 向服务器发起通信(recvfrom/sendo)
  3. 关闭套接字

基于UDP的socket编程流程图

  基于UDP的socket编程不需要设置监听和发起/接收请求,可以直接相互通信,流程如下:

在这里插入图片描述
服务器端代码:

#include "stdafx.h"
#include <Winsock2.h>     // windows socket的套接字库

int _tmain(int argc, _TCHAR* argv[])
{
    WORD dwVersionReq = MAKEWORD(1,1);
    WSAData wsData = {0};
    // **加载套接字库**  ??
    if (0 != WSAStartup(dwVersionReq,&wsData))
    {
        WSACleanup();
        return 0;
    }
    //socket版本确认
    if (LOBYTE(wsData.wVersion) != 1 || HIBYTE(wsData.wVersion != 1))
    {
        WSACleanup();
        return 0;
    }

    //创建服务套接字
    SOCKET socketSrv = socket(AF_INET,SOCK_DGRAM,0/*自动选择协议*/);

    //对socket绑定端口号和IP地址
    SOCKADDR_IN addrServer;
    addrServer.sin_family = AF_INET;
    addrServer.sin_port = htons(6000);
    addrServer.sin_addr.S_un.S_addr = htonl(ADDR_ANY);
    bind(socketSrv,(SOCKADDR*)&addrServer,sizeof(addrServer));


    //保存client端socket信息
    SOCKADDR_IN addClient;
    int len = sizeof(SOCKADDR_IN);

    while (1)
    {
        //等待并接收数据
        char szBuffer[100] = {0};
        recvfrom(socketSrv,szBuffer,100,0,(SOCKADDR*)&addClient,&len);
        printf("client->server:%s\n",szBuffer);
        //发送数据
        char szSendBuf[100] = "this is server";
        sendto(socketSrv, szSendBuf, 100,0,(SOCKADDR*)&addClient,len);
    }


    //关闭套接字
    closesocket(socketSrv);

    //**关闭winsocket库**
    WSACleanup();
    
	// 从程序中发起系统命bai令行调du用,执行命令:pause
	// 等待用户信号;不然控制台程序会一闪即过,你来不及看到执行结果。
    system("pause");

    return 0;
}

客户端代码:

#include "stdafx.h"
#include <Winsock2.h>

int _tmain(int argc, _TCHAR* argv[])
{
    //指定wind socket版本
    WORD dwVersionReq = MAKEWORD(1,1);
    WSAData wsData = {0};

    //加载套接字库dll
    if (0 != WSAStartup(dwVersionReq,&wsData))
    {
        WSACleanup();
        return 0;
    }

    //版本判断
    if (LOBYTE(wsData.wVersion) != 1 || HIBYTE(wsData.wVersion != 1))
    {
        WSACleanup();
        return 0;
    }

    //创建套接字
    SOCKET socketClient = socket(AF_INET,SOCK_DGRAM,0);

    //填充服务器socket信息
    SOCKADDR_IN addrServer;
    int nLength = sizeof(addrServer);
    addrServer.sin_family = AF_INET;
    //服务器的应用程序端口
    addrServer.sin_port = htons(6000);
    //服务器的IP地址,同一台电脑就是本地IP
    addrServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

    //发送数据
    char szClientBuf[100]= "this is client";
    sendto(socketClient,szClientBuf,100,0,(SOCKADDR*)&addrServer,nLength);
    
    //介绍数据
    char szRecvBuf[100] = {0};
    recvfrom(socketClient,szRecvBuf,100,0,(SOCKADDR*)&addrServer,&nLength);
    printf("server->client:%s\n", szRecvBuf);

    //关闭套接字
    closesocket(socketClient);

    //关闭winsocket库
    WSACleanup();

    system("pause");
    return 0;
}

以上内容部分转载自:
https://www.cnblogs.com/straight/articles/7660889.html
转自csdn学友

MFC的socket网络编程

MFC之CAsyncSocket详解

  CAsyncSocket类是从Object类派生而来。CAsyncSocket对象称为异步套接字对象。使用CAsyncSocket进行网络编程,可以充分利用Windows操作系统提供的消息驱动机制,通过应用程序框架来传递消息,方便地处理各种网络事件。另一方面,作为MFC微软基础类库中的一员,CAsyncSocket可以和MFC的其他类融为一体,大大扩展了网络编程的空间,方便了编程。

使用CAsyncSocket的一般步骤

  网络应用程序一般采用客户端/服务器模式,他们使用的CAsyncSocket编程有所不同,下面以表格的形式方式看一下服务器和客户端之间的不同

笔记整理网址:https://blog.csdn.net/u012372584/article/details/76146844

序号服务端客户端
1构造一个套接字 CAsyncSocket sockServer构造套接字 CAsyncSocket sockClient
2创建SOCKET句柄,绑定端口 sockServer.Create(nPort);创建SOCKET句柄,使用默认参数 sockClient.Create();
3启动监听 sockServer.Listen();
4请求连接服务器端 sockClient.Connect(strAddress, nPort);
5构造一个新套接字 CAsyncSocket sockRecv; 接收连接请求 sockServer.Accept(sockRecv); (注意:不需要创建SOCKET句柄, 因为Accept()会返回该句柄);
6接收数据 sockRecv.Receive(pBuffer, nLen);发送数据 sockClient.Send(pBuffer, nLen);
7发送数据 sockRecv.Send(pBuffer, nLen);接收数据 sockClient.Receive(pBuffer, nLen);
8关闭套接字 sockRecv.Close();关闭套接字 sockClient.Close();

 注意:sockRecv.Close(); 执行后,sockServer并没有关闭;一个服务器端程序的sockServer的生命周期一般与程序相同;

 注意:Accept是将一个新的空CAsyncSocket对象作为它的参数,在调用Accept之前必须构造这个对象。与客户端套接字的连接是通过它建立的,如果这个套接字对象退出,连接也就关闭。对于这个新的套接字对象,不需要调用Create来创建它的底层套接字;

  关闭并销毁CAsyncSocket对象。

  • 如果在堆栈上创建了套接字对象,当包含此对象的函数退出时,会自动调用该类的析构函数,销毁该对象。在销毁该对象之前,析构函数会调用该对象的Close成员函数。
  • 如果在堆上使用new创建了套接字对象,可先调用Close成员函数关闭它,在使用delete来删除释放该对象

  在使用CAsyncSocket进行网络通信时,我们还需要处理以下几个问题:

  1. 堵塞处理,CAsyncSocket对象专用于异步操作,不支持堵塞工作模式,如果应用程序需要支持堵塞操作,必须自己解决

  2. 字节顺序的转换。在不同的结构类型的计算机之间进行数据传输时,可能会有计算机之间字节存储顺序不一致的情况。用户程序需要自己对不用的字节顺序进行转换

  3. 字符串转换。同样,不同结构类型的计算机的字符串存储顺序也可能不同,需要自行转换,如Unicode和ANSI字符串之间的转换

创建CAsyncSocket对象

创建异步套接字对象一般是分为两个步骤,首先要构造CAsyncSocket对象,其次创建该对象底层的SOCKET句柄

1.创建空的CAsyncSocket对象

通过调用CAsyncSocket构造函数,创建一个新的空CAsyncSocket套接字对象,构造函数还带参数。套接字对象创建之后必须调用他的成员函数来创建底层的套接字数据结构,并绑定他的地址

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值