Socket通讯

时间:2022-05-21 11:28:20

复习贴,资料大多来自百科.看了一遍理解了一遍,把绕口的话按语义给改了`_>`

对于一个网络连接来说,套接字是平等的,并没有差别,不因为在服务器端或在客户端而产生不同级别。不管是Socket还是ServerSocket它们的工作都是通过SocketImpl类及其子类完成的。

用户开始Socket连接时, 首先申请一个Socket号码;同时要知道对方的号码. 然后向对方发出连接请求(假如对方不在同一局域网相当于给出网络地址). 假如对方在场并空闲(相当于通信的另一主机开机且可以接受连接请求),双方就可以Socket通信, 连接成功。双方建立连接通讯过程,向Socket发送数据和从socket接收数据。通话结束后,一方挂起电话机相当于关闭Socket,撤消连接。


连接过程

连接的三次握手:服务器监听,客户端请求,连接确认。
(1)服务器监听:是服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态。
(2)客户端请求:客户端发起连接请求,请求服务端的Socke. 客户端要先确认服务器的套接字,指出服务器端Socket的地址和端口号,然后发起连接请求。
(3)连接确认:当服务器端监听到客户端的连接请求, 它就响应客户端请求建立一个新的线程. 把服务器端套接字的描述发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

 
实现函数
 
创建一个Socket:
int socket(int domain, int type, int protocol);
参数说明:
  
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)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。
protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
注意:1.type和protocol不可以随意组合,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当第三个参数为0时,会自动选择第二个参数类型对应的默认协议。
2.WindowsSocket下protocol参数中不存在IPPROTO_STCP
  
返回值:调用成功返回创建的Socket id. 失败返回INVALID_SOCKET(Linux下失败返回-1). 
每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。

绑定

函数原型:
int bind(SOCKET socket, const struct sockaddr* address, socklen_t address_len);
参数说明:
socket:是一个套接字描述符。
address:是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。
address_len:确定address缓冲区的长度。
返回值:
如果函数执行成功,返回值为0,否则为SOCKET_ERROR。

接收

函数原型:
int recv(SOCKET socket, char FAR* buf, int len, int flags);
参数说明: 
socket:一个标识已连接套接口的描述字。
buf:用于接收数据的缓冲区。
len:缓冲区长度。
flags:指定调用方式。取值:MSG_PEEK 查看当前数据,数据将被复制到缓冲区中,但并不从输入队列中删除;MSG_OOB 处理带外数据。
返回值:
若无错误发生,recv()返回读入的字节数。如果连接已中止,返回0。否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
函数原型:
ssize_t recvfrom(int sockfd, void buf, int len, unsigned int flags, struct socketaddr* from, socket_t* fromlen);
参数说明:
sockfd:标识一个已连接套接口的描述字。
buf:接收数据缓冲区。
len:缓冲区长度。
flags:调用操作方式。是以下一个或者多个标志的组合体,可通过or操作连在一起:
(1)MSG_DONTWAIT:操作不会被阻塞;
(2)MSG_ERRQUEUE: 指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅佐性消息的方式传递进来,使用者应该提供足够大的缓冲区。导致错误的原封包通过msg_iovec作为一般的数据来传递。导致错误的数据报原目标地址作为msg_name被提供。错误以sock_extended_err结构形态被使用。
(3)MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
(4)MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长, 只对packet套接字有效。
(5)MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号,错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。
(6)MSG_EOR:指示记录的结束,返回的数据完成一个记录。
(7)MSG_TRUNC:指明数据报尾部数据已被丢弃,因为它比所提供的缓冲区需要更多的空间。
  
/*(MSG_TRUNC使用错误,4才是MSG_TRUNC的正确解释)*/
(8)MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。
(9)MSG_OOB:指示接收到out-of-band数据(即需要优先处理的数据)。
(10)MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其它数据。
from:(可选)指针,指向装有源地址的缓冲区。
fromlen:(可选)指针,指向from缓冲区长度值。

发送

函数原型:
int sendto( SOCKET s, const char FAR* buf, int size, int flags, const struct sockaddr FAR* to, int tolen);
参数说明:
s:套接字
buf:待发送数据的缓冲区
size:缓冲区长度
flags:调用方式标志位, 一般为0, 改变Flags,将会改变Sendto发送的形式
addr:(可选)指针,指向目的套接字的地址
tolen:addr所指地址的长度
返回值:
如果成功,则返回发送的字节数,失败则返回SOCKET_ERROR。

接收连接请求

函数原型:
int accept( int fd, struct socketaddr* addr, socklen_t* len);
参数说明:
fd:套接字描述符。
addr:返回连接着的地址
len:接收返回地址的缓冲区长度
返回值:
成功返回客户端的文件描述符,失败返回-1。
 

 
Linux下的Socket
/* File Name: server.c */
#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 DEFAULT_PORT 8000 //默认端口
#define MAXLINE 4096 //最大行数
int main(int argc, char** argv)
{
int socket_fd, connect_fd;
struct sockaddr_in servaddr;
char buff[];
int n;
//初始化Socket
if( (socket_fd = socket(AF_INET, SOCK_STREAM, )) == - ) //流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。数据报式Socket(SOCK_DGRAM)是一种无连接的Socket,对应于无连接的UDP服务应用。
   {
  printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);
  exit();
}
//初始化
memset(&servaddr, , sizeof(servaddr));
servaddr.sin_family = AF_INET; //AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址设置成INADDR_ANY,让系统自动获取本机的IP地址。 htonl,host to network, l 的意思是返回类型是long  将主机数转换成无符号长整形的网络字节顺序。
servaddr.sin_port = htons(DEFAULT_PORT);//设置的端口为DEFAULT_PORT htons将一个无符号短整型的主机数值转换为网络字节顺序,即大尾顺序(big-endian) //将本地地址绑定到所创建的套接字上
if( bind(socket_fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -)
   {
  printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno);
  exit();
}
//开始监听是否有客户端连接
if( listen(socket_fd, ) == -)
  {
  printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno);
  exit();
}
printf("======waiting for client's request======\n");
while()
  {
//阻塞直到有客户端连接,不然多浪费CPU资源。
  if( (connect_fd = accept(socket_fd, (struct sockaddr*)NULL, NULL)) == -)
     {
  printf("accept socket error: %s(errno: %d)",strerror(errno),errno);
  continue;
  }
//接受客户端传过来的数据
  n = recv(connect_fd, buff, MAXLINE, );
//向客户端发送回应数据
  if(!fork())
  { /*子进程*/
  if(send(connect_fd, "Hello,you are connected!\n", ,) == -)
  perror("send error");
  close(connect_fd);
  exit();
  }
  buff[n] = '\0';
  printf("recv msg from client: %s\n", buff);
  close(connect_fd);
}
close(socket_fd);
}
/* File Name: client.c */  

#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,rec_len;
char recvline[], sendline[];
char buf[MAXLINE];
struct sockaddr_in servaddr;

if( argc != )
   {
  printf("usage: ./client <ipaddress>\n");
  exit();
} if( (sockfd = socket(AF_INET, SOCK_STREAM, )) < )
   {
  printf("create socket error: %s(errno: %d)\n", strerror(errno),errno);
  exit();
} memset(&servaddr, , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons();
if( inet_pton(AF_INET, argv[], &servaddr.sin_addr) <= )
  {
  printf("inet_pton error for %s\n",argv[]);
  exit();
  }
if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < )
  {
  printf("connect error: %s(errno: %d)\n",strerror(errno),errno);
  exit();
}
printf("send msg to server: \n");
fgets(sendline, , stdin);
if( send(sockfd, sendline, strlen(sendline), ) < )
{
  printf("send msg error: %s(errno: %d)\n", strerror(errno), errno);
  exit();
}
if((rec_len = recv(sockfd, buf, MAXLINE,)) == -)
  {
perror("recv error");
exit();
}
buf[rec_len] = '\0';
printf("Received : %s ",buf);
close(sockfd);
exit();
}
 
 
 
Windows下的Socket
//Server服务端
#include<winsock2.h>
#include<stdio.h>
#pragmacomment(lib,"ws2_32.lib")
void main()
{
WSADATA wsaData;
SOCKET sockServer;
SOCKADDR_IN addrServer;
SOCKET sockClient;
SOCKADDR_IN addrClient;
WSAStartup(MAKEWORD(,),&wsaData);
sockServer=socket(AF_INET,SOCK_STREAM,);
addrServer.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//INADDR_ANY表示任何IP
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons();//绑定端口6000
bind(sockServer,(SOCKADDR*)&addrServer,sizeof(SOCKADDR)); //Listen监听端
listen(sockServer,);//5为等待连接数目
printf("服务器已启动:\n监听中...\n");
int len=sizeof(SOCKADDR);
charsendBuf[];//发送至客户端的字符串
charrecvBuf[];//接受客户端返回的字符串 //会阻塞进程,直到有客户端连接上来为止
sockClient=accept(sockServer,(SOCKADDR*)&addrClient,&len);
//接收并打印客户端数据
recv(sockClient,recvBuf,,);
printf("%s\n",recvBuf); //关闭socket
closesocket(sockClient);
WSACleanup();}
//Client客户端
#include<winsock2.h>
#include<stdio.h>
#pragmacomment(lib,"ws2_32.lib")
void main()
{
WSADATA wsaData;
SOCKET sockClient;//客户端Socket
SOCKADDR_IN addrServer;//服务端地址
WSAStartup(MAKEWORD(,),&wsaData);
//新建客户端socket
sockClient=socket(AF_INET,SOCK_STREAM,);
//定义要连接的服务端地址
addrServer.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//目标IP(127.0.0.1是回送地址)
addrServer.sin_family=AF_INET;
addrServer.sin_port=htons();//连接端口6000
//连接到服务端
connect(sockClient,(SOCKADDR*)&addrServer,sizeof(SOCKADDR));
//发送数据
charmessage[]="HelloSocket!";
send(sockClient,message,strlen(message)+,);
//关闭socket
closesocket(sockClient);
WSACleanup();}

 
三次握手与四次断开<转>
连接握手:
.客户端发送建立连接请求
.服务端确认连接请求
.客户端确认已经连接
以上3步完成后即可使用send recv了
断开握手:如果要正确断开必须经过4次握手。
.客户端发送请求停止TCP连接请求
.服务端收到请求后将这一个socket关闭
.服务端发送反向请求客户端关闭socket连接
.客户端确认服务端请求,关闭socket连接。 TCP是一个面向连接的服务,面向连接的服务是电话系统服务模式的抽象,每一次完整的数据传输都必须经过建
立连接,数据传输和终止连接3个过程,TCP建立连接的过程称为三次握手,下面看一下三次握手的具本过程
TCP三次握手过程
主机A通过向主机B 发送一个含有同步序列号的标志位的数据段给主机B ,向主机B 请求建立连接,通过这个数据段,
主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我.
主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:
我已经收到你的请求了,你可以传输数据了;你要用哪佧序列号作为起始数据段来回应我
主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:"我已收到回复,我现在要开始传输实际数据了 这样3次握手就完成了,主机A和主机B 就可以传输数据了.
3次握手的特点
没有应用层的数据
SYN这个标志位只有在TCP建产连接时才会被置1
握手完成后SYN标志位被置0 TCP建立连接要进行3次握手,而断开连接要进行4次,这是由于TCP的半关闭造成的,因为TCP连接是全双工的(
即数据可在两个方向上同时传递)所以进行关闭时每个方向上都要单独进行关闭,这个单方向的关闭就叫半关闭.
关闭的方法是一方完成它的数据传输后,就发送一个FIN来向另一方通告将要终止这个方向的连接.当一端收到一个FIN,它必须
通知应用层TCP连接已终止了这个方向的数据传送,发送FIN通常是应用层进行关闭的结果. 4次断开
当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求
主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1
由B 端再提出反方向的关闭请求,将FIN置1
主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束.
由TCP的三次握手和四次断开可以看出,TCP使用面向连接的通信方式,大大提高了数据通信的可靠性,使发送数据端
和接收端在数据正式传输前就有了交互,为数据正式传输打下了可靠的基础

TODO:Socket的几个IO模型