什么是socket
之前我们在谈论进程间通信的时候,说到过一种实现进程间通信的机制,就是socket套接字,那么socket到底是什么呢?
来看看百度百科的解释:
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。建立网络通信连接至少要一对socket。socket本质是编程接口(API)
在网络中我们利用IP地址+端口号来表示网络中的唯一一个进程,所以IP地址+端口号就称为socket
socket相关操作
socket相当于是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。
1.创建socket文件描述符
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:即协议域,又称为协议族。常用的协议域有,AF_INET、AF_INET6、AF_LOCAL、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合。(在后面我们都使用的是AF_INET)
type:指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等
来解释一下其中两种类型:
- SOCK_STREAM:提供有序的、可靠的、双向的和基于连接的字节流(TCP)
- SOCK_DGRAM:无连接的、不可靠的和使用固定大小缓冲区的数据报服务(UDP)
protocol:指定协议,在这里不多做解释,通常设置为0
返回值:创建成功返回一个socket文件描述符,唯一表示一个socket。失败返回-1。
当我们调用socket创建一个socket时,返回的socket描述符它存在于协议族空间中,但没有一个具体的地址,其实在调用socket函数创建socket时,内核还并未给socket分配源地址和源端口。如果想要给它赋值一个地址,就必须调用bind()函数。
2.绑定端口号
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd:要绑定的socket描述符
addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同
来看看socket不同协议的地址类型格式:
在头文件netinet/in.h中,我们可以找到IPV4和IPV6的地址格式,在这里主要看看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 */
//填充字段
unsigned char sin_zero[sizeof (struct sockaddr) -
__SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) -
sizeof (struct in_addr)];
};
/* Internet address. */
typedef uint32_t in_addr_t;//相当于32位无符号整型
struct in_addr
{
in_addr_t s_addr;
};
我们可以看出后的是我们使用的地址为sockaddr_in类型,所以在使用过程中需要强制类型转换为sockaddr类型
addrlen:对应地址的长度
返回值:成功返回0,失败返回-1
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
当我们给bind 函数传第二个参数的时候,需要注意一个网络字节序的问题
网络字节序与主机字节序
- 主机字节序
就是我们平常说的大端和小端模式,大端就是高地址存放低字节,小端就是高地址存放高字节。不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。 - 网络字节序
内存地址有大小端之分,网络数据流同样有大端小端之分。发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
举个例子:4个字节的32bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这就是大端字节序。也就是TCP/IP首部中所有的二进制整数在网络中传输时都要求的这种次序。
所以我们在使用的时候一定要注意主机和网络字节序直接的转换,下面的函数就派上了用场:
#include <arpa/inet.h>
/*将32位的长整数从主机字节序转换为网络字节序,*/
uint32_t htonl(uint32_t hostlong);
/*将16位的短整数从主机字节序转换为网络字节序,*/
uint16_t htons(uint16_t hostshort);
/*将32位的长整数从网络字节序转换为主机字节序,*/
uint32_t ntohl(uint32_t netlong);
/*将16位的短整数从网络字节序转换为主机字节序,*/
uint16_t ntohs(uint16_t netshort);
如果主机是小端字节序,这些函数就会将参数做相应的大小端转换然后返回; 如果主机是大端字节序那么这些函数将不做转换,将参数原封不动地返回。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3.监听socket
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd:要监听的socket
backlog: socket可以排队的最大连接个数
返回值:成功返回0,失败返回-1
4.接受请求
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。当TCP服务器监听到请求之后,就会调用accept()函数接收请求
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd:服务器的socket描述符
addr:指向struct sockaddr *的指针,用于返回客户端的协议地址(具体同bind函数)
addrlen:协议地址的长度
返回值:成功则返回一个由内核自动生成的全新描述符,代表与返回客户的TCP连接。失败返回-1。
5.建立连接
客户端通过调用connect函数来建立与TCP服务器的连接。
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
sockfd:客户端的socket描述符
addr:服务器的socket地址
addrlen: sock地址的长度
返回值:成功返回0,失败返回-1
利用TCP实现简单服务器端客户端
直接上代码:
Makefile
.PHONY:all
all:tcp_client tcp_server
tcp_client:tcp_client.c
gcc -o $@ $^
tcp_server:tcp_server.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f tcp_client tcp_server
server.c 服务器端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage %s ip port\n",argv[0]);
return 1;
}
//创建套接字
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
return 2;
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(atoi(argv[2]));
local.sin_addr.s_addr=inet_addr(argv[1]);
//绑定
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
return 3;
}
//监听
if(listen(sock,5)<0)
{
perror("listen");
return 4;
}
while(1)
{
char buf[1024];
buf[0]=0;
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//接受请求
int newsock=accept(sock,(struct sockaddr *)&peer,&len);
if(newsock<0)
{
perror("accept");
return 5;
}
inet_ntop(AF_INET,&peer.sin_addr,buf,sizeof(buf));
printf("get a connect,ip:%s,port:%d\n",buf,ntohs(peer.sin_port));
while(1)
{
ssize_t s=read(newsock,buf,sizeof(buf));
if(s>0)
{
buf[s]=0;
printf("[%s:%d] %s",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port),buf);
}
else
{
printf("client quit\n");
break;
}
write(newsock,buf,strlen(buf)+1);
}
}
close(sock);
return 0;
}
client.c 客户端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char* argv[])
{
if(argc!=3)
{
printf("Usage %s ip port\n",argv[0]);
return 1;
}
//创建套接字
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0)
{
perror("socket");
return 2;
}
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(atoi(argv[2]));
server.sin_addr.s_addr=inet_addr(argv[1]);
//建立连接
if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0)
{
perror("connect");
return 3;
}
while(1)
{
char buf[1024];
buf[0]=0;
printf("Please Enter:");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf));
if(s>0)
{
buf[s]=0;
}
write(sock,buf,sizeof(buf));
if(strncmp(buf,"quit",4)==0)
{
break;
}
read(sock,buf,sizeof(buf));
printf("server echo:%s",buf);
}
close(sock);
return 0;
}
利用本地IP来测试程序,跨网络的测试读者可以自行测试:
这次实现的网络程序只是一个单进程的版本,之后会实现一种多进程和多线程版本。