本文先从TCP套接字讲起,其次回顾http报文,最后将tinyhttp的源代码进行解析
第一部分 Linux网络编程:TCP socket套接字详解
标识通信端点(对外):IP地址+端口号
操作系统/进程如何管理套接字(对内):套接字描述符
一个进程中的多线程对一个套接字的使用无计数
基本概念
1.socket
socket这个词可以表示很多概念: 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程,“IP地址+端口号”就称为socket。
在TCP协议中,建立连接的两个进程各自有一个socket来标识,那么这两个socket组成 的socketpair就唯一标识一个连接。 socket本身
有“插座”的意思,因此用来描述网络连接的一 对一关系。
2.网络字节序
发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出,接收主机把从网络上接到的字节依次保存在接收缓冲区中,
也是按内存地址从低到高的顺序保存,因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//32位的主机字节转换为网络字节
uint16_t htons(uint16_t hostshort);//16位的主机字节转换为网络字节
uint32_t ntohl(uint32_t netlong);//32位的网络字节转换为主机字节
uint16_t ntohs(uint16_t netshort);//16位的网络字节转换为主机字节
3.socket地址的数据类型及相关函数
struct sockaddr_in
{
u_char sin_len;//地址长度
u_char sin_family;//地址族(TCP/IP:AF_INET)
u_short sin_port;//端口号
struct in_addr sin_addr;//32位IP地址
char sin_zero[8];//置0
}
使用TCP/IP协议族的网络应用程序声明端口地址变量时,使用结构sockaddr_in
IP地址十进制点分字符串与32位IP地址转换
//字符串转in_addr
#include <sys/socket.h>
#include <netinet/in.h>
#include <apra/inet.h>
int inet_aton(const char *cp,struct in_addr *)
in_addr_t inet_addr(const char *cp);
//in_addr转字符串:
char *inet_ntoa(struct in_addr in);
服务端的流程
1.创建套接字
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain,int type,int protocol);
domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,UnixSocket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:指定Socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式Socket(SOCK_STREAM)是一种面向连接的Socket,针对于面向连接的TCP服务应用。
protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。一般设为0,会自动选择第二个参数类型对应的默认协议。
返回值信息:如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1)
套接字描述符:是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表但是套接字数据结构都是在操作系统的内核缓冲里。
2.绑定:
将一本地地址与一套接口捆绑。本函数适用于未连接的数据报或流类套接口,在connect()或listen()调用前使用。当用socket()创建套接口后,它便存在于一个名字空间(地址族)中,但并未赋名。bind()函数通过给一个未命名套接口分配一个本地名字来为套接口建立本地捆绑(主机地址/端口号)。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr * addr,socklen_t addrlen);
socket:是一个套接字描述符。
address:是一个sockaddr结构指针,该结构中包含了要结合的地址和端口号。
address_len:确定address缓冲区的长度。
返回值:如果函数执行成功,返回值为0,否则为SOCKET_ERROR,返回-1
3.监听:
创建一个套接口,并监听申请的连接
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd,int backlog);
sockfd:用于标识一个已捆绑未连接套接口的描述字。
backlog:等待连接队列的最大长度。一般5-10 不可过大
4.获取连接:
在套接口接受一个连接
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen );
- 参数sockfd
参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个 套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。
- 参数addr
这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结 构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL。
- 参数addrlen
如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。
但是,如果你给accept函数第2和第3个参数赋值后,就要记得“用之”,否则就会报错。
5.关闭套接字
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#define _BACKLOG_ 10
int start(const char* _ip, int _port)
{
int sock = socket(AF_INET, SOCK_STREAM, 0);//1.创建套接字 返回值相当文件描述符
if(sock < 0)
{
perror("socket");
exit(2);
}
//绑定
struct sockaddr_in server_socket;
server_socket.sin_family = AF_INET;
server_socket.sin_port = htons(_port);
server_socket.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock, (struct sockaddr*)&server_socket, sizeof(server_socket)) < 0)
{
perror("bind error!");
exit(3);
}
if(listen(sock, _BACKLOG_) < 0)
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argc, char* argv[])
{
if(argc != 3)
{
printf("argv");
return 1;
}
int listen_sock = start(argv[1],atoi(argv[2]));
struct sockaddr_in remote;
int len = sizeof(remote);
char buf[1024];
while(1)
{
int sock = accept(listen_sock, (struct sockaddr*)&remote,&len);
if(sock < 0)
{
perror("accept error");
continue;
}
printf("connect... ip is: %s port is: %d \n", inet_ntoa(remote.sin_addr), ntohs(remote.sin_port));
while(1)
{
ssize_t s = read(sock, buf, sizeof(buf)-1);
if(s > 0)
{
buf[s] = '\0';
printf("client :# %s\n", buf);
write(sock, buf,strlen(buf));
}
else
{
printf("client close....\n");
break;
}
}
}
close(listen_sock);
return 0;
}
客户端流程:
(1)创建套接字(socket)
(2)向服务器发出连接请求(connect)
(3)和服务器端进行通信(read/write)
(4)关闭套接字
其中需要调用connect进行连接服务器
#include <sys/types.h>
#include <sys/socket.h>
int connect