SOKET 篇(一)入门简介
什么是socket?
Socket不仅可以用于本地进程间的通信,也可以用于网络间不同主机间的通信。
Tcp/ip 协议栈已经属于内核的一部分了,应用程序是在用户空间的。
地址结构
不同主机之间通信,一定要知道相互的地址和端口号;一次连接的四个要素为:客户端ip,客户端port,服务端ip,服务端port。linux 下可以通过man 7 ip 命令查看地址结构。
ipv4地址结构:
struct sockaddr_in{
unit8_t sin_len; // struct size 一些平台没有
sa_family_t sin_family; //address family: ipv4——>AF_INET
in_port_t sin_port; //port in network byte order 16位
struct in_addr sin_addr; // iternet address
char size_zero[8];// 保留字段
};
utruct in_addr{
unit32_t s_addr;
};
ipv6将所有in字段全部改为in6即可。eg:struct sockaddr_in6;struct in6_addr sin6_addr;
通用套接字地址结构:
struct sockaddr{
uint8_t sa_len; //整个地址长度
sa_family_t sa_family; // 地址簇
char sa_data[14];//由sa_family 决定它的形式 14字节,同上面最后3项
};
在bind等函数进行参数传递时要将套接字地址强制转换成通用套接字结构。
其中地址簇使用AF_前缀,而像后面的socket函数中domain使用PF_前缀。AF_前缀表示地址簇,PF_ 前缀表示协议簇。
字节序
- 大端字节序:最高有效位存储在最低地址处,最低有效位存储在高地址处;
- 小端字节序:最低有效位存储在最低地址处,最高有效位存储在高地址处;
- 主机字节序:不同主机有不同字节序,通常我们使用的Intel 的cpu为小端字节序,而AMD的cpu是大端;
- 网络字节序:是大端字节序。
可以编写一个测试字节序的小函数:
int main(){
unsigned int a = 0x12345678;
unsigned char* b =(unsigned char*)&a;
if(b[0] == 0x78)
printf("little-endian");
else
printf("big-endian");
return 0;
}
字节序转换
unit32_t htonl(unit32_t hostlong);
unit16_t htons(unit16_t hostshort);
unit32_t ntohl(unit32_t netlong);
unit16_t ntohs(unit16_t netshort);
上面函数名称中,h代表host,n代表network, s代表short,l代表long。
地址转换函数:
#include<netinet/in.h>
#include<arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);//功能同inet_addr
in_addr_t inet_addr(const char *cp); // 点分ip地址转换成32位网络字节序整数
char *inet_ntoa(struct in_addr in);// 地址结构转换成点分式ip地址
套接字的类型
流式套接字(SOCK_STREAM):提供面向连接的,可靠数据传输服务,如对应于TCP协议;
数据报套接字(SOCK_DGRAM):提供面向连接的,非可靠数据报传输服务,如对应于UDP;
原始套接字(SOCK_RAM):将应用层数据直接跨越传输层直接对ip层进行封装。
socket函数
头文件:<sys/socket.h>
功能:创建套接字;
函数原型:
int socket(int domain, int type, int protocol);
参数:其中domain表示协议簇,type表示套接字类型(SOCK_STREAM, SOCK_DGRAM等),protocol表示协议。通常前两项确定了protocol也就确定了,可以设置为0。
返回值:成功返回一个非负数,代表套接字描述符(文件描述符的一种,linux中一切皆文件),简称套接字。失败返回-1。
bind函数
头文件:<sys/socket.h>
功能:绑定本机地址到socket创建的套接字;
函数原型:
int bind(int sockfd, const struct sockaddr*addr, socklen_t addrlen);
参数: sockefd指socket函数返回的套接字;addr表示要绑定的地址(通用地址结构,ipv4要强制转换);addrlen代表地址长度。
返回值:成功返回0,失败返回-1。
listen函数
头文件:<sys/socket.h>
功能:将socket产生的套接用于监听接入的连接,是将主动套接字变成了被动套接字。主动套接字通过connect发起连接,被动套接字通过accept接受连接。
函数原型:
int listen(int sockfd, int backlog);
参数: sockefd指socket函数返回的套接字;backlog规定内核为此套接字排队的最大连接数(即规定的是并发连接数)。
返回值:成功返回0,失败返回-1。
下面是直接从书上copy的图:
对于给定的监听套接字,内核会维护两个队列(他们之和不能超过backlog):
1.已由客户端发送到服务端,服务器正在等待完成tcp三次握手过程;
2.已完成三次握手连接的队列,accept接收到之后,会将接收到的已完成连接从该队列删除,以便由更多的客户端发起连接。
accept函数
头文件:<sys/socket.h>
功能:从已完成连接队列中返回第一个连接,如果已完成连接队列为空,则阻塞。
函数原型:
int accept(int sockfd, struct sockaddr*addr, socklen_t* addrlen);
参数: sockfd:服务器套接字,addr返回对等方的套接字地址,addrlen返回对等方套接字地址长度;
返回值:成功返回非负整数,失败返回-1。(返回的是主动套接字)
connect函数
头文件:<sys/socket.h>
功能:从已完成连接队列中返回第一个连接,如果已完成连接队列为空,则阻塞。
函数原型:
int connect(int socked, const struct sockaddr*addr, socklen_t addrlen);
参数: sockfd服务器套接字,addr服务器套接字地址,addrlen服务器套接字地址长度;
返回值:成功返回0,失败返回-1。
简单的回射服务器
client端:
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<string.h>
#define err_exit(m)\
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main(void){
int sockfd;
if((sockfd = socket(PF_INET, SOCK_STREAM,0)) < 0)
err_exit("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
//servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//inet_aton("127.0.0.1", &servaddr.sin_addr);
if(connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
err_exit("connect");
char sendbuf[1024] = {0};
char recvbuf[1024] = {0};
while(fgets(sendbuf,sizeof(sendbuf), stdin) != NULL){
write(sockfd, sendbuf, strlen(sendbuf));
read(sockfd, recvbuf, sizeof(recvbuf));
fputs(recvbuf, stdout);
memset(sendbuf, 0, sizeof(sendbuf));
memset(recvbuf, 0, sizeof(recvbuf));
}
close(sockfd);
return 0;
}
server端代码:
#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<stdio.h>
#include<errno.h>
#include<string.h>
#define err_exit(m)\
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main(void){
int listenfd;
if((listenfd = socket(PF_INET, SOCK_STREAM,0)) < 0)
err_exit("socket");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
//inet_aton("127.0.0.1", &servaddr.sin_addr);
if(bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
err_exit("bind");
if(listen(listenfd, SOMAXCONN) < 0)
err_exit("listen");
struct sockaddr_in peeraddr;
socklen_t peerlen = sizeof(peeraddr);//如果不初始化大小,accept会出错
int conn;
if((conn = accept(listenfd, (struct sockaddr*)&peeraddr, &peerlen)) < 0)
err_exit("accept");
char buf[1024];
int n = 0;
while(1){
memset(&buf, 0, sizeof(buf));
if((n = read(conn, buf, sizeof(buf))) < 0)
err_exit("read");
fputs(buf, stdout);
write(conn, buf, n);
}
close(listenfd);
close(conn);
return 0;
}
TCP客户/服务器总体流程
客户端:
创建套接字sock;
指定服务器地址与端口等信息(sockaddr_in信息);
调用connect()建立连接connect;
通过sock套接字进行交互。
服务端:
创建套接字listenfd;
指定本机套接字地址;
绑定本机套接字地址,调用bind();
监听套接字listen(),并给出最大连接数(两个队列,已完成三次握手的连接队列,和未完成队列);
定义连接对等方的地址,并且初始化其地址长度。调用accept()返回一个主动套接字,和对等方的套接字地址。
通过accept返回的主动套接字进行交互。
一次连接的四个要素:客户端ip、客户端port、服务端ip、服务端port。