socket和tcp/ip协议的关系
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
1、Socket基本概念
-
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。
-
建立网络通信连接至少要一对端口号(socket)。socket本质是编程接口(API),对TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口;HTTP是轿车,提供了封装或者显示数据的具体形式;Socket是发动机,提供了网络通信的能力。
-
Socket的英文原义是“孔”或“插座”。作为BSD UNIX的进程通信机制,取后一种意思。通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供220伏交流电, 有的提供110伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。
| 中文名 | 套接字 |
|---|---|
| 常用类型 | 流式socket和数据包式socket |
| 相关模式 | 对等模式 、C/S模式 |
| 相关应用 | c++、java、python |
- 应用程序通常通过“套接字”向网络发出请求或者应答网络请求。
- socket实际上提供进程通信的端点。进程通信之前,双方首先必须各自创建一个端点,否则是没有办法建立联系并相互通信的。
- 在网间网内部,每一个Socket用一个半相关模式:(协议,本地地址,本地端口)
- 一个完整的socket有一个本地唯一的Socket号,由操作系统分配。
- Socket是面向客户/服务器模型而设计的,针对客户和服务器程序提供不同的Socket系统调用。客户随机申请一个Socket(相当于一个想打电话的人可以在任何一台入网电话上拨号呼叫),系统为之分配一个Socket号;服务器拥有全局公认的Socket,任何客户都可以向它发出连接请求和信息请求(相当于一个被呼叫的电话拥有一个呼叫方知道的电话号码)。 Socket利用客户/服务器模式巧妙地解决了进程之间建立通信连接的问题。服务器Socket半相关为全局所公认非常重要。读者不妨考虑一下,两个完全随机的用户进程之间如何建立通信?假如通信双方没有任何一方的Socket固定,就好比打电话的双方彼此不知道对方的电话号码,要通话是不可能的。
2、 套接字连接的步骤
套接字之间的连接过程可以分为3个步骤:
-
服务器监听
是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。 -
客服端请求‘
客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端套接字提出连接请求。 -
连接确认
当服务器套接字监听到或收到客户端套接字的连接请求,它就响应客户端套接字的请求,建立一个新的线程,把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接受其他客户端套接字的连接请求。
3、常用函数
下面以TCP为例,介绍几个基本的socket接口函数。
3.1、 socket()
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
-
函数原型:
int socket(int domain,int type,int protocol); -
参数说明
-
domain:协议域,又称协议族。
1)常用的协议族有:AF_INET,AF_INET6,AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。2)协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
3)用于确定哪个通讯模式。
-
type:指定socket类型。
1) 常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。2) 流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。
3)数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。
-
protocol:指定协议。
1)常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等。
协议 对应的协议 PROTO_TCP TCP传输协议 ROTO_UDP UDP传输协议 ROTO_STPC STCP传输协议 PROTO_TICP TIPC -
-
注意:
- type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。
- WindowsSocket下protocol参数中不存在IPPROTO_STCP
-
返回值
若调用成功,返回新创建的套接字的描述符,
若失败,返回INVALID_SOCKET(Linux下返回-1)。
套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
3.2、绑定
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
-
函数原型
int WSAAPI bind( SOCKET s, const struct sockaddr FAR * name, int namelen ); -
参数说明:
- socket
一个套接字的描述符,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是给这个描述字绑定一个名字。 - socketAddr*
是一个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 */ };- address_len
对应的是地址的长度。
- socket
-
返回值:
成功:返回0
失败:返回SOCKET_ERROR。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
网络字节序和主机字节序
主机字节序:是我们平常说的大端和小端模式:不同的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。
3.3、listen()和connect()
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
-
listen()
为了接受连接,先用socket()创建一个套接口的描述字,然后用listen()创建套接口并为申请进入的连接建立一个后备日志,然后便可用accept()接受连接了。listen()仅适用于支持连接的套接口,如SOCK_STREAM类型的。套接口s处于一种“变动”模式,申请进入的连接请求被确认,并排队等待被接受。这个函数特别适用于同时有多个连接请求的服务器;如果当一个连接请求到来时,队列已满,那么客户将收到一个WSAECONNREFUSED错误。- 函数原型
int WSAAPI listen( SOCKET s, 第一个参数即为要监听的socket描述字 int backlog 第二个参数为相应socket可以排队的最大连接个数。 ); - 返回值
如无错误发生,listen()返回0。
否则的话,返回-1。
应用程序可通过WSAGetLastError()获取相应错误代码。
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
- 函数原型
-
connect()
本函数用于创建与指定外部端口的连接。s参数指定一个未连接的数据报或流类套接口。如套接口未被捆绑,则系统赋给本地关联一个唯一的值,且设置套接口为已捆绑。请注意若名字结构中的地址域为全零的话,则connect()将返回WSAEADDRNOTAVAIL错误。流类套接口(SOCK_STREAM类型),利用名字来与一个远程主机建立连接,一旦套接口调用成功返回,它就能收发数据了。对于数据报类套接口(SOCK_DGRAM类型),则设置成一个缺省的目的地址,并用它来进行后续的send()与recv()调用。
-
函数原型
int WSAAPI connect( SOCKET s, 第一个参数即为客户端的socket描述字 const struct sockaddr FAR * name, 第二参数为服务器的socket地址 int namelen 第三个参数为socket地址的长度 ); -
返回值
若无错误发生,则connect()返回0。
否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。对非阻塞套接口而言,若返回值为SOCKET_ERROR则应用程序调用WSAGetLastError()。如果它指出错误代码为WSAEWOULDBLOCK,则您的应用程序可以:
1.用select(),通过检查套接口是否可写,来确定连接请求是否完成。
2.如果您的应用程序使用基于消息的WSAAsyncSelect()来表示对连接事件的兴趣,则当连接操作完成后,您会收到一个FD_CONNECT消息
客户端通过调用connect函数来建立与TCP服务器的连接。
-
3.4、 accept()函数
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
accept()是在一个套接口接受的一个连接。accept()是c语言中网络编程的重要的函数,本函数从s的等待连接队列中抽取第一个连接,创建一个与s同类的新的套接口并返回句柄。
-
函数原型
SOCKET WSAAPI accept( SOCKET s, 第一个参数为服务器的socket描述字 struct sockaddr FAR * addr, 第二个参数为指向struct sockaddr *的指针,指向一缓冲区, 用于返回客户端的协议地址,实际格式由通讯时产生的地址族确定 int FAR * addrlen 第三个参数为指向存有addr地址长度的整型数。addrlen参数也是一个返回参数,在调用时初始化为addr所指的地址空间;在调用结束时它包含了实际返回的地址的长度(用字节数表示) ); -
返回值
若成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。
若失败,返回INVALID_SOCKET错误,应用程序可通过调用WSAGetLastError()来获得特定的错误代码。
注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
3.5 read() 和 write() 等函数
至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:
recvmsg()/sendmsg() : 这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。
1. read()/write()
unistd.h是用于linux/unix系统的调用。
是POSIX标准定义的unix类系统定义符号常量的头文件,
包含了许多UNIX系统服务的函数原型,例如read函数、write函数和getpid函数,
这个函数是依赖于编译器,依赖于操作系统的。
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
- read()
- 功能:read函数是负责从fd中读取内容.
- 返回值:
当读成功时,read返回实际所读的字节数,
如果返回的值是0,表示已经读到文件的结束了;
小于0,表示出现了错误。
如果错误为EINTR,说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。
- write()
- 功能:write函数将buf中的nbytes字节内容写入文件描述符fd.
- 返回值:
成功时返回写的字节数。
失败时返回-1,并设置errno变量。
在网络程序中,当我们向套接字文件描述符写时有俩种可能。
2. recv()/send()
int
WSAAPI
recv(
_In_ SOCKET s, 一个标识已经连接套接口的描述字。
_Out_ char FAR * buf, 用于已经接收数据的缓冲区
_In_ int len, 缓冲区长度
_In_ int flags 指定调用方式。
);
int
WSAAPI
send(
_In_ SOCKET s, 一个用于标识已连接套接口的描述字。
_In_reads_bytes_(len) const char FAR * buf, 包含待发送数据的缓冲区。
_In_ int len, 缓冲区中数据的长度。
_In_ int flags 调用执行方式。
);
-
recv()
-
windows版本:
- 第四个参数:
MSG_PEEK 查看当前数据。数据将被复制到缓冲区中,但并不从输入队列中删除。
MSG_OOB 处理带外数据。 - 返回值:
若无错误发生,recv()返回读入的字节数。
如果连接已中止,返回0。
否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
错误代码:
- 第四个参数:
-
linux版本:
- 第四个参数:
MSG_DONTROUTE 绕过路由表查找。
MSG_DONTWAIT 仅本操作非阻塞。
MSG_OOB 发送或接收带外数据。
MSG_PEEK 窥看外来消息。
MSG_WAITALL 等待所有数据。 - 返回值:
若无错误发生,recv()返回读入的字节数。
如果连接已中止,返回0。
如果发生错误,返回-1,应用程序可通过perror()获取相应错误信息。
- 第四个参数:
这里只描述同步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信号,进程对该信号的默认处理是进程终止。 -
-
send()
- 返回值:
若无错误发生,send()返回所发送数据的总数(请注意这个数字可能小于len中所规定的大小)。
否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
send()适用于已连接的数据包或流式套接口发送数据。对于数据报类套接口,必需注意发送数据长度不应超过通讯子网的IP包最大长度。IP包最大长度在WSAStartup()调用返回的WSAData的iMaxUdpDg元素中。如果数据太长无法自动通过下层协议,则返回WSAEMSGSIZE错误,数据不会被发送。
请注意成功地完成send()调用并不意味着数据传送到达。
如果传送系统的缓冲区空间不够保存需传送的数据,除非套接口处于非阻塞I/O方式,否则send()将阻塞。对于非阻塞SOCK_STREAM类型的套接口,实际写的数据数目可能在1到所需大小之间,其值取决于本地和远端主机的缓冲区大小。可用select()调用来确定何时能够进一步发送数据。 - 返回值:
3. readv()/writev()
UNIX和WINSOCK提供了不同的实现方法UNIX系统下,使用writev,可以指定一系列的缓冲区,收集要写的数据,使可以安排数据保存在多个缓冲区中,然后同时写出去,从而避免出现Nagle和延迟ACK算法的相互影响。
#include <sys/uio.h>
ssize_t writev( int fd, const struct iovec *iov, int cnt );
ssize_t readv( int fd, const struct iovec *iov, int cnt );
-
writev()
-
功能:writev将多个数据存储在一起,将驻留在两个或更多的不连接的缓冲区中的数据一次写出去。
-
返回值:传输字节数,出错时返回-1.
-
参数说明:
fd:文件描述符
iov:是一组iovec结构的指针,iovec结构如下:char *iov_base; /*基本地址指针,指向缓冲区*/ size_t iov_len; /*指定缓冲区长度*/ };说明:这个定义取自FreeBSD系统,许多系统现在定义基本地址指针为void *iov_base;
cnt是数组中iovec结构的个数,即分开缓冲区的个数。
- readv()
- 功能: readv将读入的数据散布读到缓冲区中。readv总是先填满一个缓冲区,然后再填写下一个。
- 返回值:readv返回读到的总字节数。如果遇到文件结尾,已无数据可读,则返回0。若出错则返回-1。
- 参数
fd:文件描述符
iov 指向iovec结构数组的一个指针。
iovcnt 数组元素的个数
这两个函数可以用于套接字及任何类型的文件描述符。
-
4. recvmsg()/sendmsg()
这两个函数是最通用的I/O函数。实际上我们可以把所有read、readv、recv和recvfrom调用替换成recvmsg调用。类似地,各种输出函数调用也可以替换成sendmsg调用。
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
返回:读入或写出字节数——成功; -1——出错
详细: https://www.cnblogs.com/nufangrensheng/p/3607267.html
5. recvfrom()/sendto()
int
WSAAPI
recvfrom(
_In_ SOCKET s, 标识一个已经连接套接口的描述字。
_Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, 接收数据缓冲区
_In_ int len, 接收数据缓冲区的长度
_In_ int flags, 调用操作方式。
_Out_writes_bytes_to_opt_(*fromlen, *fromlen) struct sockaddr FAR * from, 指针,指向装有源地址的缓冲区。
_Inout_opt_ int FAR * fromlen 指针,指向from缓冲区长度值
);
int
WSAAPI
sendto(
_In_ SOCKET s, 标识一个已经连接套接口的描述字。
_In_reads_bytes_(len) const char FAR * buf,
_In_ int len,
_In_ int flags,
_In_reads_bytes_(tolen) const struct sockaddr FAR * to,
_In_ int tolen
);
-
recvfrom()
-
调用操作方式。是以下一个或者多个标志的组合体,可以通过or操作连在一起:
MSG_DONTWAIT 操作不会阻塞。 MSG_ERRQUEUE 指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过msg_iovec作为一般的数据来传递。导致错误的数据报原目标地址作为msg_name被提供。错误以sock_extended_err结构形态被使用。 MSG_PEEK 指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。 MSG_TRUNC 返回封包的实际长度,即使它比所提供的缓冲区更长。只对Packet套接字有效。 MSG_WAITALL 要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。 MSG_EOR 指示记录的结束,返回的数据完成一个记录。 MSG_TRUNC 指明数据报尾部数据已被丢弃,因为它比所提供的缓存区需要更多的空间。 MSG_CTRUNC 指明由于缓冲区空间不足,一些控制数据已被丢弃。 MSG_OOB 指示接收到out-of-band数据。即需要优先处理的数据。 MSG_ERRQUEUE 指示除了来自套接字错误队列的错误外,没有接受到其它数据。 -
函数说明
recvfrom()用来接收远程主机经指定的socket传来的数据,并把数据传到由参数buf指向的内存空间,参数len为可接收数据的最大长度.参数flags一般设0。 -
返回值
成功则返回接收到的字符数,
失败返回-1.
-
-
sendto()
- 参数说明
参数 参数说明 socket 套接字 buf 待发送数据的缓冲区 size 缓冲区长度 flags 调用方式标志位,一般为0,改变Flags,将会改变sendto发送的形式 addr(可选) 指向目的的套接字的地址 tolen addr所指地址的长度 - 返回值
若成功,返回发送的字节数
若失败,返回SOCKET_ERROR
3.6、close()函数
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
int
WSAAPI
closesocket(
_In_ SOCKET s
);
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
4、如何告知对方已发送完命令
其实这个问题还是比较重要的,正常来说,客户端打开一个输出流,如果不做约定,也不关闭它,那么服务端永远不知道客户端是否发送完消息,那么服务端会一直等待下去,直到读取超时。所以怎么告知服务端已经发送完消息就显得特别重要。
-
通过Socket关闭
当Socket关闭的时候,服务端就会收到响应的关闭信号,那么服务端也就知道流已经关闭了,这个时候读取操作完成,就可以继续后续工作。但是这种方式有一些缺点
- 客户端Socket关闭后,将不能接受服务端发送的消息,也不能再次发送消息
- 如果客户端想再次发送消息,需要重现创建Socket连接
-
通过socket关闭输出流的方式
这种方式调用的方法是: socket.shutdownOutput(); 而不是(outputStream为发送消息到服务端打开的输出流): outputStream.close();- 如果关闭了输出流,那么相应的Socket也将关闭,和直接关闭Socket一个性质。调用Socket的shutdownOutput()方法,底层会告知服务端我这边已经写完了,那么服务端收到消息后,就能知道已经读取完消息,如果服务端有要返回给客户的消息那么就可以通过服务端的输出流发送给客户端,如果没有,直接关闭Socket。
- 这种方式通过关闭客户端的输出流,告知服务端已经写完了,虽然可以读到服务端发送的消息,但是还是有一点点缺点:
不能再次发送消息给服务端,如果再次发送,需要重新建立Socket连接
这个缺点,在访问频率比较高的情况下将是一个需要优化的地方。
-
通过约定的符号
-
这种方式的用法,就是双方约定一个字符或者一个短语,来当做消息发送完成的标识,通常这么做就需要改造读取方法。
假如约定单端的一行为end,代表发送完成,例如下面的消息,end则代表消息发送完成:
what is your name?
end此时需要改造服务器的读取方式:
Socket socket = server.accept(); // 建立好连接后,从socket中获取输入流,并建立缓冲区进行读取 BufferedReader read=new BufferedReader(new InputStreamReader(socket.getInputStream(),"UTF-8")); String line; StringBuilder sb = new StringBuilder(); while ((line = read.readLine()) != null && "end".equals(line)) { //注意指定编码格式,发送方和接收方一定要统一,建议使用UTF-8 sb.append(line); }可以看见,服务端不仅判断是否读到了流的末尾,还判断了是否读到了约定的末尾。
-
优缺点如下:
优点:不需要关闭流,当发送完一条命令(消息)后可以再次发送新的命令(消息)
缺点:需要额外的约定结束标志,太简单的容易出现在要发送的消息中,误被结束,太复杂的不好处理,还占带宽
-
拆包和黏包
使用Socket通信的时候,或多或少都听过拆包和黏包,如果没听过而去贸然编程那么偶尔就会碰到一些莫名其妙的问题,所有有这方面的知识还是比较重要的,至少知道怎么发生,怎么防范。
现在先简单说明下拆包和黏包的原因:
- 拆包:当一次发送(Socket)的数据量过大,而底层(TCP/IP)不支持一次发送那么大的数据量,则会发生拆包现象。
- 黏包:当在短时间内发送(Socket)很多数据量小的包时,底层(TCP/IP)会根据一定的算法(指Nagle)把一些包合作为一个包发送。
首先可以明确的是,大部分情况下我们是不希望发生拆包和黏包的(如果希望发生,什么都去做即可),那么怎么去避免呢,下面进行详解?
-
黏包
首先我们应该正确看待黏包,黏包实际上是对网络通信的一种优化,假如说上层只发送一个字节数据,而底层却发送了41个字节,其中20字节的I P首部、 20字节的T C P首部和1个字节的数据,而且发送完后还需要确认,这么做浪费了带宽,量大时还会造成网络拥堵。当然它还是有一定的缺点的,就是因为它会合并一些包会导致数据不能立即发送出去,会造成延迟,如果能接受(一般延迟为200ms),那么还是不建议关闭这种优化,如果因为黏包会造成业务上的错误,那么请改正你的服务端读取算法(协议),因为即便不发生黏包,在服务端缓存区也可能会合并起来一起提交给上层,推荐使用长度+类型+数据模式。
如果不希望发生黏包,那么通过禁用TCP_NODELAY即可,Socket中也有相应的方法:
void setTcpNoDelay(boolean on)
通过设置为true即可防止在发送的时候黏包,但是当发送的速率大于读取的速率时,在服务端也会发生黏包,即因服务端读取过慢,导致它一次可能读取多个包。 -
拆包
最大报文段长度(MSS)表示TCP传往另一端的最大块数据的长度。当一个连接建立时,连接的双方都要通告各自的 MSS。客户端会尽量满足服务端的要求且不能大于服务端的MSS值,当没有协商时,会使用值536字节。虽然看起来MSS值越大越好,但是考虑到一些其他情况,这个值还是不太好确定。
如何应对拆包,那就是如何表明发送完一条消息了,对于已知数据长度的模式,可以构造相同大小的数组,循环读取,示例代码如下:int length=1024;//这个是读取的到数据长度,现假定1024 byte[] data=new byte[1024]; int readLength=0; while(readLength<length){ int read = inputStream.read(data, readLength, length-readLength); readLength+=read; }
这样当循环结束后,就能读取到完整的一条数据,而不需要考虑拆包了。
还有很多关于socket编程问题,请参考 https://www.cnblogs.com/yiwangzhibujian/p/7107785.html
学习:
socket:
https://www.cnblogs.com/suntp/p/6434644.html
握手:https://blog.csdn.net/muyang3433/article/details/79478138