在Linux中的网络编程是通过socket接口来进行的。其实socket接口也是一种特殊的I/O(在《深入理解计算机系统》这本书中的IO部分也有提到网络也是一种特殊的IO),它也是一种文件描述符。socket也有一个类似于打开文件的函数调用,该函数返回一个整型的socket描述符,随后的建立连接、数据传输等操作都是通过socket来实现的。
1 .流式socket(SOCK_STREAM)
流式套接字使用的是TCP协议,由于TCP协议建立在三次握手的基础上,所以这种类型能够提供可靠的、面向连接的通信流,能够保证数据传输的正确性和顺序性。
2.数据报socket(SOCK_DGRAM)
数据报套接字使用的是UDP协议,由于UDP将数据扔出去之后就不管的桀骜特性,所以该类型定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证是可靠、无差错的。
3.原始socket
原始套接字允许对底层协议如IP或ICMP(在网络层,而TCP和UDP都在传输层)进行直接访问,功能比较强大但是使用不便,主要用于一些协议的开发。
在介绍socket函数的使用之前还需要介绍一下保存socket信息的结构体。在C语言中有两个重要的struct数据类型:sockaddr和sockaddr_in,这两个结构体都是用用来存储socket的相关信息的。
struct sockaddr {
unsigned short sa_family;//地址族,2字节
char sa_data[14];//14字节的协议地址,包含该socket的IP地址和端口号,14字节
}
struct sockaddr_in {
short sa_family;//地址族,2字节
unsigned short int sin_port;//端口号,2字节
struct in_addr sin_addr;//IP地址,4字节
unsigned char sin_zero[];//填充0以保持与struct sockaddr同样大小,8字节
}
在sockaddr_in中使用了in_addr的结构体:
struct in_addr {
in_addr_t s_addr;
};
表示一个32位的IPv4地址。
in_addr_t一般为32位的unsigned int,其字节顺序为网络字节序,即该无符号数采用大端字节序。其中每8位表示一个IP地址中的一个数值。
struct sockaddr是通用的套接字地址,而struct sockaddr_in则是internet环境下套接字的地址形式。
这两种结构体都是16字节,并且都存在family属性,不同的是:sockaddr使用的是14字节的sa_data,而sockaddr_in将sa_data的14字节拆分成了sin_port和sin_addr以及sin_zero。
所以这两种数据包含的内容都是一样的,明显sockaddr_in类型将地址和端口拆开更为方便(sockaddr给操作系统使用),所以一般使用sockaddr_in进行填充然后将其转换为sockaddr。
有人注意到这两种结构体都存在sa_family字段,sa_family可选的常见值定义在#include < netinet/in.h >文件下,其中可选的值有以下几种:
地址的内容可以看看这片文章:IP地址的三种表示格式 及 在Socket编程中的应用
其中的重点部分:
IP地址其实有三种不同的表示格式:
1)Ascii(网络点分字符串)
2)网络地址(32位无符号整形,网络字节序,大头)
3)主机地址 (主机字节序)
在Socket编程开发中,通过函数inet_addr和inet_ntoa可以实现点分字符串与网络字节顺序格式IP地址之间的转换。
在Socket编程中,有四个函数来完成主机字节顺序格式和网络字节顺序格式之间的转换,它们是:htonl、htons、ntohl、和ntohs。 htons和ntohs完成16位无符号数的相互转换,htonl和ntohl完成32位无符号数的相互转换。
在Linux中有一些函数可以实现主机名和地址的转换,最为常见的有gethostbyname、gethostbyaddr、getaddrinfo等,它们都可以实现IPv4和IPv6的地址和主机名之间的转换。
本人对这地址之间的关系也是一头雾水,所以不做过多解释。
先看一下使用TCP和UDP的流程图:
TCP:
UDP:
函数的作用顾名思义,使用socket建立一个socket链接,然后使用sockaddr_in进行初始化以保存所建立的socket信息。然后使用bind函数将IP地址和端口号进行绑定(用于TCP链接,UDP无需)。然后使用listen开启监听,在服务器使用accept等待链接,客户端使用connect进行链接等等。
socket:
头文件:#include < sys/socket.h>
函数原型:int socket(int family,int type,int protocol)
参数含义:
family:对应的就是AF_INET、AF_INET6等。
type:套接字类型:SOCK_STREAM、SOCK_DGRAM、SOCK_RAW。
protocol:0
返回值:
成功:非负套接字描述符。
出错:-1
bind:
头文件:#include< sys/socket.h>
函数原型:int bind(int sockfd,struct sockaddr *my_addr,int addrlen)
参数含义:
sockfd:套接字描述符。
my_addr:本地地址。
addrlen:地址长度:
返回值:
成功:0
出错:-1
listen:
头文件:#include < sys/socket.h>
函数原型:int listen(int sockfd,int backlog)
参数含义:
sockfd:套接字描述符
backlog:请求队列中允许的最大请求数,大多数系统默认为20
返回值:
成功:0
出错:-1
accept:
头文件:#include < sys/socket.h>
函数原型:int accept(int sockfd,struct sockaddr * addr,socklen_t* addrlen)
参数含义:
sockfd:套接字描述符
addr:客户端地址
addrlen:地址长度
返回值:
成功:0
出错:-1
connect:
头文件:#include < sys/socket.h>
函数原型:int connect(int sockfd,struct sockaddr* serv_addr,int addrlen)
参数含义:
sockfd:套接字描述符
serv_addr:服务器端地址
addrlen:地址长度
返回值:
成功:0
出错:-1
send
头文件:#include < sys/socket.h>
函数原型:int send(int sockfd,const void* msg,int len,int flags)
参数含义:
sockfd:套接字描述符
msg:指向要发送数据的指针
len:数据长度
flags:一般为0
返回值:
成功:发送的字节数
出错:-1
recv
头文件:#include < sys/socket.h>
函数原型:int recv(int sockfd,void* buf,int len,unsigned int flags)
参数含义:
sockfd:套接字描述符
buf:存放接受数据的缓冲区
len:数据长度
flags:一般为0
返回值:
成功:接受的字节数
出错:-1
sendto
头文件:#include < sys/socket.h>
函数原型:int sendto(int sockfd,const void* msg,int len,unsigned int flags,const struct sockaddr* to,int tolen)
参数含义:
sockfd:套接字描述符
msg:指向要发送数据的指针
len:数据长度
flags:一般为0
to:目标机的IP地址和端口号信息
tolen:地址长度
返回值:
成功:发送的字节数
出错:-1
recvfrom
头文件:#include < sys/socket.h>
函数原型:int recvfrom(int sockfd,void * buf,int len,unsigned int flags,struct sockaddr* from,int* fromlen)
参数含义:
sockfd:套接字描述符
buf:存放接收数据的缓冲区
len:数据长度
flags:一般为0
from:源机的IP地址和端口号信息
tolen:地址长度
返回值:
成功:接收的字节数
出错:-1
一个简单的实例:客户端将“hello”传给服务器。
server.c
#include
#include
#include
#include
#include
#include
#include
#define SERVPORT 3333//定义端口号
#define BACKLOG 10//请求队列中允许的最大请求数
#define MAXDATASIZE 5//数据长度
int main() {
struct sockaddr_in server_sockaddr,client_sockaddr;//声明服务器和客户端的socket存储结构
int sin_size,recvbytes;
int sockfd,client_fd;//socket描述符
char buf[MAXDATASIZE];//传输的数据
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) {//建立socket链接
perror("Socket");
exit(1);
}
printf("Socket success!,sockfd=%d\n",sockfd);
//以sockaddt_in结构体填充socket信息
server_sockaddr.sin_family = AF_INET;//IPv4
server_sockaddr.sin_port = htons(SERVPORT);//端口
server_sockaddr.sin_addr.s_addr = INADDR_ANY;//本主机的任意IP都可以使用
bzero(&(server_sockaddr.sin_zero),8);//填充0
if((bind(sockfd,(struct sockaddr *)&server_sockaddr,sizeof(struct sockaddr))) == -1) {//bind函数绑定
perror("bind");
exit(-1);
}
printf("bind success!\n");
if(listen(sockfd,BACKLOG) == -1) {//监听
perror("listen");
exit(1);
}
printf("listening ... \n");
if((client_fd = accept(sockfd,(struct sockaddr *) &client_sockaddr,&sin_size)) == -1) {//等待客户端链接
perror("accept");
exit(1);
}
printf("accept success!\n");
if((recvbytes = recv(client_fd,buf,MAXDATASIZE,0)) == -1) {//接收客户端的请求
perror("recv");
exit(1);
}
printf("received a connection : %s\n",buf);
close(sockfd);
}
client.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define SERVPORT 3333
#define MAXDATASIZE 100
int main(int argc,char *argv[]) {
int sockfd,sendbytes;
char buf[MAXDATASIZE];
struct hostent* host;
struct sockaddr_in serv_addr;
if(argc < 2) {//需要用户指定链接的地址
fprintf(stderr,"Please enter the server's hostname");
exit(1);
}
if((host = gethostbyname(argv[1])) == NULL) {//转换为hostent
perror("gethostbyname");
exit(1);
}
if((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1) {//创建socket
perror("socket");
exit(1);
}
//填充数据
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVPORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero),8);
if((connect(sockfd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr))) == -1) {//发起对服务器的链接
perror("connect");
exit(1);
}
if((sendbytes = send(sockfd,"hello",5,0)) == -1) {//发送消息给服务器端
perror("send");
exit(1);
}
close(sockfd);
}
运行结果:先运行server开启监听等待链接,
再运行client。