什么是套接字(Socket)?
TCP用主机的IP地址加上主机上的端口号作为TCP连接的端点,这种端点就叫做套接字或插口。这里可以将套接字理解为一个“中间人”的角色,在TCP/IP通讯模型中,两台主机是不能直接进行通信的,需要通讯必须经过套接字,通讯双方将需要通讯的信息交给各自的套接字,然后由这两个套接字进行通信,套接字是支持网络通讯的基本单元。套接字包含网络通信的基本信息:(1)源主机IP地址、(2)源主机端口号、(3)目标主机IP地址、(4)目标主机端口号、(5)连接使用的协议。(通过IP地址和端口号可以唯一的确定一台的主机上的一个应用)
常用的TCP/IP协议的3中套接字的类型:
(1)流式套接字(SOCK_STREAM)
流式套接字用于提供面向连接的、可靠的数据传输服务。该服务将保证数据能够实现误差错、无重复发送,并按顺序接收。流式套接字采用的协议是TCP(面向字节流)。
(2)数据报套接字(SOCK_DGRAM)
数据报套接字提供了一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数量重复,且无法保证顺序的接收到数据。数据报套接字使用的协议是UDP(面向报文)。
(3)原始套接字(SOCK_RAW)
原始套接字允许对较低的层次的协议直接访问,比如IP,ICMP协议,常用于检验新的协议实现,或者访问现有服务配置的新设备。
原始套接字和标准套接字(流式、数据报)的区别在于:原始套接字可以读写内存中没有处理的IP数据报,而标准套接字只能 读取TCP/UDP协议的数据,因此如果需要访问其他协议发送的数据就必须使用原始套接字。
这里线介绍两组函数,之后可以用到。
IP地址转化函数
#include <arpa/inet.h>
int inet_pton(int af,const char *src,void* dst);
const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);
支持IPV4和IPV6,函数可重入。
inet_pton:将“点分十进制”转化为“二进制整数”。将转化后的内容存放在dst中。dst的类型可以时ipv4或ipv6类型。
inet_ntop:将“二进制整数”转化为“点分十进制”。将转化的内容存放在dst中。
sockaddr数据结构
struct socket很多网络编程函数诞生于早于IPV4协议,现在已经很少使用了,但为了向前兼容,现在sockaddr退化成了(void*)的作用,传递一个地址给函数,至于这个函数是sockaddr_in还是sockaddr_in6,由地址族确定,然后函数内部在强制类型转化位所需的地址类型。
#include <netinet/in.h>
struct sockaddr
{
sa_family_t sa_family; //地址族
char sa_data[14];//14个字节,包含套接字中目标地址和端口信息
};
struct sockaddr_in
{
sa_family_t sin_family; //地址族 AF_INET(IPV4)
uint16_t sin_port; //端口号
struct in_addr sin_addr; //IP地址
char sin_zero[8] //8字节的填充
};
struct in_addr
{
uint32_t s_addr;
};
TCP网络编程架构
TCP网络编程有两种模式,一种是服务器模式,另一种是客户端模式。服务器模式。服务器模式创建服务程序等待客户端程序连接,接收到用户的连接请求后,根据用户的请求进行处理;客户端模式则根据目的服务器的地址和端口进行连接,向服务器发送请求并对服务器的响应进行数据处理。
网络套接字函数
1、创建网络端口函数socket()
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
函数功能:打开一个网络通讯端口。
参数说明:
domain:用于设置网络通信的域。
常见的网络通讯域:
AF_INET:使用TCP或UDP传输,IPV4。
AF_INET6:使用TCP或UDP传输,IPV6。
AF_UNIX:本地协议,当客户端和服务器在同一台机器上的时候使用。
type:用于设置套接字的通讯的类型。
SOCK_STREAM:流式套接字
SOCK_DGRAM:数据报套接字
SOCK_RAW:原始套接字
SOCK_RDW:提供可靠的数据报文,不过可能数据会有乱序。
SOCK_PACKET:这是一个专用类型,不能再通用程序中使用。
SOCK_SEQPACKET:序列化包,提供一个序列化的、可靠的、双向的基于连接的数据传输通道,数据长度定长。每次调用读系统调用时需要将数据全部读出。
protocl:0,采用默认协议。
返回值:成功返回新的文件描述符,失败返回-1.设置errno。
常见的错误原因:
值 | 含义 |
EACCES | 没有权限建立指定domain的type的socket |
EAFNOSUPPORT | 不支持所给的地址类型 |
EINVAL | 不支持此协议或者协议不可用 |
EMFILE | 进程文件表溢出 |
ENFILE | 已经达到系统允许打开的文件数量,打开文件过多 |
ENOBUFS/ENOMEM | 内存不足。 |
EPROTONOSUPPORT | 指定的协议在type在damain中不存在 |
2、绑定一个地址端口对bind()
在建立套接字描述符成功后,需要对套接字进行地址和端口的绑定,才能进行数据的接收和发送的操作。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd,const struct sockadder *my_addr,socklen_t addrlen);
函数功能:
将套接字和IP地址、端口绑定在一起。因为服务的地址和端口号是固定的,所以需要绑定,但对于客户端则不需要。
参数说明:
sockfd:socket()函数创建的文件描述符。
my_addr:是一个指向结构为socksddr的参数的指针,sockaddr中包含了IP地址和端口的信息。
addlen:my_addr的结构体的长度。
返回值:成功返回0,失败返回-1,并设置errno。
3、监听本地端口listen()
#include <sys/socket.h>
int listen(int sockfd,int backlog);
函数功能:
初始化服务器可连接队列,并监听端口。
参数说明:
sockfd:socket的文件描述符。
backlog:已建立连接队列和等待建立连接队列的长度的总和。
返回值:成功返回0,失败返回-1,并设置errno。
系统默认的backlog存放在 /proc/sys/net/ipv4/tcp_max_syn_backlog中,可以通过cat命令查看。
如果指定的值小于系统规定的值,那么backlog的值为指定的值,就为系统规定的值。
4、接收一个网络请求accept()
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
函数功能:
接收一个客户端的连接。如果服务调用accept()时还没有客户端的连接请求,就阻塞等待,直到客户端连接上来。
参数说明:
sockfd:socket文件描述符。
addr:传出参数,返回连接客户端的地址信息,包括IP地址和端口号。
addrlen:传入传出参数(值-结果),传入sizeof(addr)大小,函数返回真正的接收到的地址结构体大小。
返回值:成功返回一个新的socket描述符(用于和客户端通信)失败返回-1,并设置errno
accept函数时处理客户端连接的,但accept一次只能处理一个客户端,如果同时有很多个客户端同时连接,来不及处理的客户端就会处理待处理队列中,等待建立连接。如果已建立连接队列和待处理的队列的总和超过backlog,服务器会直接发送一个RST复位包。
程序:简单的服务器模型
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main()
{
int listenSock;
int clientSock;
char ipStr[128];
listenSock=socket(AF_INET,SOCK_STREAM,0); //创建一个使用网络IPV4,运输TCP的套接字
struct sockaddr_in serAddr; //存放服务器监听套接字IP地址和端口号
struct sockaddr_in cliAddr; //存放服务中负责和客户端通信的套接字的IP地址和端口号
bzero(&(serAddr),sizeof(serAddr)); //将结构体数据清零
bzero(&(cliAddr),sizeof(cliAddr));
serAddr.sin_family=AF_INET; //设置协议,需要和套接字中的相同
serAddr.sin_port=htons(5500); //设置端口号!(1-1024)
serAddr.sin_addr.s_addr=htonl(INADDR_ANY); //IP为本地的IP。0.0.0.0 监听本机的所以IP端口
bind(listenSock,(struct sockaddr *)(&serAddr),sizeof(serAddr));//绑定端口信息
listen(listenSock,20); //监听端口,backlog=20
printf("SERVER LISTENING...\n");
while(1)
{
int len=sizeof(cliAddr);
clientSock=accept(listenSock,(struct sockaddr *)(&cliAddr),&len);//处理连接
printf("Client IP:%s",inet_ntop(AF_INET,&(cliAddr.sin_addr.s_addr),ipStr,sizeof(ipStr)));
printf("ipstr %s\n",ipStr);
printf("client prot:%d\n",ntohs(cliAddr.sin_port));
close(clientSock);
}
close(listenSock);
return 0;
}
还没编写客户端可以利用nc命令来测:
nc -nv ip地址 端口号
5、连接目标网络服务器connect()
客户端建立连接之后,不需要进行地址绑定,就可以直接连接服务器。连接服务器的函数为connent(),此函数连接指定的服务器。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd,struct sockaddr *my_addr,int addrlen);
客户端需要调用connect()连接服务器,connect和bind的参数形式一致,区别在于bind的参数自己的地址,而connent的参数是对方的地址。成功返回0.失败返回-1.
程序二:简单客户端的程序
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#define IPSIZE 128
int main()
{
int cliSock=socket(AF_INET,SOCK_STREAM,0);
struct sockaddr_in serAddr;
bzero(&(serAddr),sizeof(serAddr));
serAddr.sin_family=AF_INET;
serAddr.sin_port=htons(5500);
char strSerIP[IPSIZE];
scanf("%s",strSerIP);
struct in_addr serIP;
inet_pton(AF_INET,strSerIP,(void*)(&serIP));
serAddr.sin_addr.s_addr=serIP.s_addr;
if(connect(cliSock,(struct sockaddr*)(&serAddr),sizeof(serAddr))==-1)
{
printf("连接失败!\n");
exit(1);
}
printf("连接成功!\n");
close(cliSock);
return 0;
}
因为网卡时单网卡,所以每次的IP地址都是一样的,但端口号不相同。