Socket编程编程(TCP)
Socket是BSD提供的网络应用编程接口,现在它已经是网络编程中的标准。
Socket是一种特殊的进程间通信方式,不同机器上的进程都可以使用这种方式进行通信
- 网络中的数据传输是一种I/O操作
- read、write、close操作可应用于Socket描述符
- Socket是一种文件描述符,代表了一个通信管道的一个端点
- 在Socket类型的文件描述符上,可以完成建立连接,数据传输等操作
常用的Socket类型有两种:
5. 流式Socket:SOCK_STREAM,提供面向连接的Socket
6. 数据报式Socket:SOCK_DGRAM,提供面向无连接的Socket
字节序
概念:是指多字节数据的存储顺序
分类:
- 小端格式:将低位字节数据存储在低地址
- 大端格式:将高位字节数据存储在低地址
特点:
- 网络协议指定了通讯字节序 —— 大端
- 只有在多字节数据处理时才需要考虑字节序
- 运行在同一台计算机上的进程相互通信时,一般不用考虑字节序
- 异构计算机之间通讯,需要转换自己的字节序为网络字节序
判断大小端示例代码:
1 #include <stdio.h>
2
3 union
4 {
5 short s;
6 char c[sizeof(short)];
7 }un;
8
9 int main(int argc ,char *argv[])
10 {
11 un.s = 0x0102;
12
13 if ( (un.c[0] == 1) && (un.c[1] == 2 ) )
14 {
15 printf("Big endian\n");
16 }
17 else if( (un.c[0] == 2) && (un.c[1] == 1) )
18 {
19 printf("Little endian\n");
20 }
21
22 return 0;
23 }
运行结果:
字节序转换函数
主机字节序数据转换成网络字节序数据
uint32_t htonl(uint32_t hostint32);
uint16_t htons(uint16_t hostint16);
以上返回网络字节序数据
网络字节序数据转换成主机字节序数据
uint32_t ntohl(uint32_t netint32);
uint16_t ntohs(uint16_t netint16);
以上返回主机字节序数据
示例代码:
1 #include <stdio.h>
2 #include <arpa/inet.h>
3
4 int main(int argc ,char *argv[])
5 {
6 int a = 0x01020304;
7 short int b = 0x0102;
8
9 printf("htonl(%08x) = %08x\n",a,htonl(a));
10 printf("htons(%04x) = %04x\n",b,htons(b));
11
12 return 0;
13 }
运行结果:
sockaddr_in套接字地址结构:(重点)
头文件:#include <netinet/in.h>
在IPv4因特网域(AF_INET)中,套接字地址结构用sockaddr_in命名
struct in_addr
{
in_addr_t s_addr;//4字节
};
struct sockaddr_in
{
sa_family_t sin_family; //2字节
in_port_tsin_port; //2字节
struct in_addr sin_addr; //4字节
unsigned char sin_zero[8]; //8字节
};
sockaddr通用套接字地址结构
地址标识了特定通信域中的套接字端点,地址格式与特定通信域相关,为了使不同格式地址能被传入套接字函数,地址被强制转换成通用套接字地址结构
头文件:#include <netinet/in.h>
struct sockaddr
{
sa_family_t sa_family; //2字节
char sa_data[14]; //14字节
};
inet_pton()函数
头文件:#include <arpa/inet.h>
int inet_pton(int family, const char*strptr, void *addrptr);
功能:
将点分十进制数串转换成32位无符号整数
参数:
- family : 协议族
- strptr : 点分十进制数串
- addrptr : 32位无符号整数的地址
返回值:成功:1失败:其它
inet_ntop()函数
头文件:#include <arpa/inet.h>
const char *inet_ntop(int family, constvoid *addrptr, char *strptr, size_t len);
功能:
将32位无符号整数转换成点分十进制数串
参数:
- family :协议族
- addrptr : 32位无符号整数
- strptr :点分十进制数串
- lenstrptr:缓存区长度
len的宏定义
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 46 //for ipv6
返回值:成功:则返回字符串的首地址 失败:返回NULL
创建套接字
创建套接字是进行任何网络通信时必须做的第一步
头文件:#include <sys/socket.h>
int socket(int family, int type,int protocol);
功能:创建一个用于网络通信的I/O描述符(套接字)
参数:
- family:协议族
AF_INET,AF_INET6,AF_LOCAL,AF_ROUTE,AF_KEY - type:套接字类型
SOCK_STREAM,SOCK_DGRAM,SOCK_RAW,SOCK_SEQPACKET - protocol:协议类别
0,IPPROTO_TCP,IPPROTO_UDP,IPPROTO_SCTP
返回值:套接字
socket创建的套接字特点
使用socket创建套接字时,系统不会分配端口
使用socket创建的是主动套接字,但作为服务器,需要被动等待别人的连接
服务器
做为服务器需要具备的条件:
- 具备一个可以确知的地址,以便让别人找到我
- 让操作系统知道你是一个服务器,而不是一个客户端
- 等待连接的到来,对于面向连接的TCP协议来说,连接的建立才真正意味着数据通信的开始
头文件:#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr *myaddr,socklen_t addrlen);
功能:将本地协议地址与sockfd绑定
参数:
- sockfd: socket套接字
- myaddr: 指向特定于协议的地址结构指针
- addrlen:该地址结构的长度
返回值:成功:返回 失败:其他
bind示例代码:
注意:INADDR_ANY 通配地址,值为0
头文件:#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:
- 将套接字由主动修改为被动
- 使操作系统为该套接字设置一个连接队列,用来记录所有连接到该套接字的连接
参数:
- sockfd: socket监听套接字
- backlog:连接队列的长度
返回值:成功:返回 失败:其他
listen示例代码:
int accept(int sockfd,struct sockaddr *cliaddr,socklen_t *addrlen);
功能:从已连接队列中取出一个已经建立的连接,如果没有任何连接可用,则进入睡眠等待
参数:
- sockfd: socket监听套接字
- cliaddr: 用于存放客户端套接字地址结构
- addrlen:套接字地址结构体长度
返回值:已连接套接字
注意:accept函数返回的是一个已连接套接字,这个套接字代表当前这个连接
accept示例:
客户端
作为客户端需要具备的条件:
- 知道服务器的IP地址以及端口号
int connect(int sockfd,const structsockaddr *addr,socklen_t len);
功能:
主动跟服务器建立链接
连接建立成功后才可以开始传输数据(对于TCP协议)
参数:
- sockfd:socket套接字
- addr: 需连接的服务器地址结构
- addrlen:地址结构体长度
返回值:成功:0b失败:其他
注意:connect函数建立连接之后不会产生新的套接字
connect示例:
数据传输
当连接建立后,通信的两端便具备两个套接字
套接字也是一种文件描述符,所以read、write函数可以用于从这个连接中取出或向其写入数据。
ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);
头文件:#include <sys/socket.h>
功能:用于发送数据
参数:
- sockfd: socket套接字
- buf: 待发送数据缓存区的地址znbytes: 发送缓存区大小(以字节为单位)
- flags: 套接字标志(常为0)
返回值:成功发送的字节数
注意:不能用TCP协议发送0长度的数据包
ssize_t recv(int sockfd, void *buf,size_tnbytes, int flags);
功能:用于接收网络数据
参数:
- sockfd: 套接字
- buf: 指向接收网络数据的缓冲区
- nbytes: 接收缓冲区的大小(以字节为单位)
- flags: 套接字标志(常为0)
返回值:成功接收到字节数
使用close函数即可关闭套接字
:关闭一个代表已连接套接字将导致另一端接收到一个0长度的数据包
做服务器时:
- 关闭socket创建的监听套接字将导致服务器无法继续接受新的连接,但不会影响已经建立的连接
- 关闭accept返回的已连接套接字将导致它所代表的连接被关闭,但不会影响服务器的监听
做客户端时:
:关闭连接就是关闭连接,不意味着其他
关于TCP建立和断开连接的具体过程请看我写的另一篇文章(TCP/IP)——(脑图+注释版 思路清晰)
Server端示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
char recv_buf[2048] = "";//接收缓冲区
int sockfd = 0;//定义套接字
int connfd = 0;
int err_log = 0;//错误标志
struct sockaddr_in my_addr;//服务器地址结构体
unsigned short port = 8000;//监听端口号
if(argc > 1)//传参,
{
port = atoi(argv[1]);//指定port口
}
printf("TCP Server Started at port %d!\n",port);
sockfd = socket(AF_INET,SOCK_STREAM,0);//创建TCP套接字 IPv4,流式套接字
if(sockfd < 0)
{
perror("socket");
exit(-1);
}
bzero(&my_addr,sizeof(my_addr));//清空服务器地址
my_addr.sin_family = AF_INET;//IPv4
my_addr.sin_port = htons(port);//字节序转换端口号
my_addr.sin_addr.s_addr = htonl(INADDR_ANY);//字节序转换本机的IP地址
printf("Binding server to port %d\n",port);
err_log = bind(sockfd,(struct sockaddr*)&my_addr,sizeof(my_addr));//讲服务器地址与套接字进行绑定
if(err_log != 0)
{
perror("binding");
close(sockfd);
exit(-1);
}
err_log = listen(sockfd,10);//监听客户端的连接 10个连接数
if(err_log != 0)
{
perror("listen");
close(sockfd);
exit(-1);
}
printf("Waiting client...\n");
while(1)//等待客户端的连接
{
size_t recv_len = 0;
struct sockaddr_in client_addr;//存放客户端的地址
char cli_ip[INET_ADDRSTRLEN] = "";//存放客户端IP地址 点分十进制
socklen_t cliaddr_len = sizeof(client_addr);//必须初始化
connfd = accept(sockfd,(struct sockaddr*)&client_addr,&cliaddr_len);//接收连接
if(connfd < 0)
{
perror("accept");
continue;
}
inet_ntop(AF_INET,&client_addr.sin_addr,cli_ip,INET_ADDRSTRLEN);//点分十进制的转换
printf("client ip = %s\n",cli_ip);
while( (recv_len = recv(connfd,recv_buf,sizeof(recv_buf),0)) > 0)//把接收到的数据发送回去
{
send(connfd,recv_buf,recv_len,0);
}
close(connfd);//关闭已连接的套接字
printf("client closed!\n");
}
close(sockfd);//关闭监听的套接字
return 0;
}
Client端示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(int argc,char *argv[])
{
unsigned short port = 8000;//服务器的端口号
char *server_ip = "192.168.222.183";//服务器的IP地址
char send_buf[512] = "Hello every!";//发送的数据
char recv_buf[512] = "";//接收缓冲区
int sockfd = 0;//定义套接字
int err_log = 0;//定义错误标志
struct sockaddr_in server_addr;//非通用套接字地址结构体
if( argc > 1)//利用传参,更改服务器的IP地址
{
server_ip = argv[1];
}
if( argc > 2)//利用传参,更改服务器的端口号
{
port = atoi(argv[2]);
}
bzero(&server_addr,sizeof(server_addr));//初始化服务器地址
server_addr.sin_family = AF_INET;//IPv4
server_addr.sin_port = htons(port);//端口号
inet_pton(AF_INET,server_ip,&server_addr.sin_addr);
sockfd = socket(AF_INET,SOCK_STREAM,0);//创建通信端点:套接字
if(sockfd < 0)
{
perror("socket");
exit(-1);
}
/* 连接服务器 */
err_log = connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));
if(err_log != 0)
{
perror("connect");
close(sockfd);
exit(-1);
}
send(sockfd,send_buf,strlen(send_buf),0);//向服务器发送数据
recv(sockfd,recv_buf,strlen(recv_buf),0);//接收服务器的发回的信息
printf("%s\n",recv_buf);
close(sockfd);//关闭套接字
return 0;
}