重点知识:
·IP地址、端口号、网络字节序的基本概念
·socket api的基本用法
·实现简单的UDP客户端/服务器
·实现简单的TCP客户端/服务器(单链接版本,多进程版本,多线程版本)
·理解tcp服务器建立连接,发送数据,断开连接的流程
基础概念:
IP地址:IP协议有两个版本,IPV4和IPV6,但现在我们使用的是IPV4,IPV6还没有普及,所以我们这里谈的都是IPV4.
·IP地址是在IP协议中,用来标识网络中不同主机的地址。
·IPV4的IP地址是一个4字节,32位的整数
·通常我们更习惯使用点分十进制的字符串来表示IP地址,例如 192.168.0.1;点分十进制的每一个数字表示一个字节,范围是0~255;
源IP地址和目的IP地址
在IP数据报头部中,有两个IP地址,分别表示源IP地址和目的IP地址(源IP地址即发送方的IP,目的地址即要到达的主机IP地址)
端口号
事实上,光有IP地址是没办法通信的,比如我们通过IP得知找到了主机,可是要把报文交给诸多进程中的哪一个呢,我们还需要其他的东西来标识主机上的进程,这就是端口号,端口号是唯一标示一台主机上的一个进程,这样数据报到了主机,就能找到接收报文的进程。
·端口号是传输层协议的内容,是一个2字节16位的整数;
·端口号用来标识一个进程,告诉操作系统,当前的数据报文要交给哪一个进程来处理;
·IP地址+端口号的组合就可以标识网络上的某一台主机上的某一个进程;
·一个端口号只能被一个进程占用;
·一个进程可以绑定多个端口号,但是一个端口号是不能被多个进程绑定的。
·一个网络程序对应一个端口——》一个端口对应一至多个进程—》一个进程对应一至多个线程。
源端口号和目的端口号
传输层协议(TCP/UDP)的数据段中有两个端口号,分别叫做源端口号和目的端口号,就是描述“数据是谁发的,数据是发给谁的”这个规则。
TCP/UDP初步认识
TCP:
·传输层协议
·有连接
·可靠传输
·面向字节流
UDP:
·传输层协议
·无连接
·不可靠传输
·面向数据报
网络字节序
在学习C语言的时候了解过一个知识点,机器打大端小端,实际是内存中的字节数据相对于内存地址有大端小端之分,而磁盘中的字节数据相对于文件中的偏移地址也有大端小端之分,其实网络数据流也同样有大端小端之分。
·发送主机通常将发送缓冲区中的数据按照内存地址从低到高的顺序发出
·接收主机把从网络上收到的数据依次保存在缓冲区里,也是按内存地址从低到高的顺序保存
·因此网络数据流的地址规定:先发出的数据是低地址,后发出的数据是高地址
·TCP/IP协议规定,网络数据流应该采用大端字序,即低地址高字节
·不论这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据
·如果当前机器是小断,需要转成大端,否则就忽略,直接发送
这里需要用到以下函数实现网络字节序和主机字节序的转换,使得网络程序具有可移植性,同样的代码能在大端机和小端机上都正常运行:
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); uint16_t htons(uint16_t htonshort); uint32_t ntohl(uint32_t netlong); uint16_t ntohs(uint16_t netshort); |
·h表示host(主机),n表示network(网络),l表示32位长整数,s表示16位短整数
·例如htons表示将16位的短整数从主机序转换为网络字节序。例如转换端口号
·如果主机是小端字节序,这些参数会做相应的转换然后返回
·如果主机是大端字节序,这些函数什么也不会做,将参数原原本本的返回
socket编程
socket常见API
//创建socket文件描述符(TCP/UDP,客户端+服务器) int sock(int domain,int type,int protocol); //绑定端口号(TCP/UDP,服务器) int bind(int socket,const struct sockaddr *address,socklen_t address_len); //开始监听socket(TCP,服务器) int listen(int socket,int backlog); //接受请求(TCP,服务器) int accept(int socket,struct sockaddr* address,socklen_t* address_len); //建立连接(TCP,客户端) int connect(int socket,const struct sockaddr* addr,socklen_t addrlen); |
sockaddr结构
socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPV4,IPV6,以及UNIX Domain Socket等,然而各种网络协议的地址格式是不同的。

·IPV4和IPV6的地址格式定义在netinet/in.h中,IPV4使用了sockaddr_in结构体表示,包括16位地址类型,16位端口号,32位ip地址
·IPV4和IPV6地址类型分别定义为常数AF_INET,AF_INET6,这样,只需要取得sockaddr结构体的首地址,而并不需要直到具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
·socket API可以都用struct sockaddr*来表示,在使用的时候需要强制转化为sockaddr_in;这样的好处是程序的通用性,可以接收IPV4,IPV6,以及UNIX Domain Socket各种类型的sockaddr结构体指针作为参数;
·虽然socket api的接口是sockaddr,但是我们真正在基于IPV4编程时,使用的数据结构是sockaddr_in;这个结构里主要有三部分信息:地质类型,端口号,IP地址。
网络服务器编写
UDP服务器
#include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> static void usage(const char* msg){ printf("Usage: [%s]\n”,msg,); } int main(int argc,char* argv[]){ if(argc != 3){ usage(argv[0]); return 1; } int sock = socket(AF_INET,SOCK_DGRAM,0);//创建套接字 if(sock < 0){ perror("socket"); return 2; } struct sockaddr_in server;//创建server端的sockaddr结构体 server.sin_family = AF_INET; server.sin_port = htons(atoi(argv[2])); /* server.sin_addr.s_addr = htonl(INADDR_ANY); */ server.sin_addr.s_addr = inet_addr(argv[1]); if(bind(sock,(struct sockaddr*)&server,sizeof(server)) < 0){//绑定端口号 perror("bind"); return 3; } char buf[1024]; /* memset(buf,'\0',sizeof(buf)); */ struct sockaddr_in client; for(;;){ socklen_t len = sizeof(client); ssize_t s = recvfrom(sock,buf,sizeof(buf)-1,0,(struct sockaddr*)&client,&len);、、接收消息 printf("%lu\n",s); if(s > 0){ buf[s] = '0'; printf("[%s|%d]: %s\n",inet_ntoa(client.sin_addr),htons(client.sin_port),buf); sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&client,sizeof(client));//发送回去 } } return 0; } |
UDP客户端
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> void usage(const char* msg){ printf("Usage: %s\n",msg); } int main(int argc,char* argv[]){ if(argc != 3){ usage(argv[0]); return 1; } int sock = socket(AF_INET,SOCK_DGRAM,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 = htonl(INADDR_ANY); */ server.sin_addr.s_addr = inet_addr(argv[1]); char buf[1024]; memset(buf,'\0',sizeof(buf)); struct sockaddr_in local; for(;;){ socklen_t len = sizeof(local); printf("Please Enter#: "); fflush(stdout); ssize_t s = read(0,buf,sizeof(buf)-1); if(s > 0){ buf[s-1] = '\0';//去回车 sendto(sock,&buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server));//向服务器发送消息 ssize_t _s = recvfrom(sock,&buf,sizeof(buf),0,(struct sockaddr*)&local,&len);//接受服务器返回的消息 if(_s > 0){ printf("Server echo#:%s\n",buf); } } } } |
地址转换函数
·字符串转in_addr的函数
#include <arpa/inet.h> int inet_aton(const char* strptr,struct in_addrptr); in_addr_t inet_addr(const char *strptr); int inet_pton(int family,const char* strptr,void* addrptr); |
·in_addr转字符串的函数
char* inet_ntoa(struct in_addr inaddr); const char* inet_ntop(int family,const char* addrptr,char *strptr,size_t len); |
其中inet_pton和inet_ntop不仅可以转换IPV4的in_addr,还可以转换IPV6_addr,因此函数接口是void* addrptr.
简单的TCP网络程序
TCP服务器
#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 startup(char* ip,int port){ int sock = socket(AF_INET,SOCK_STREAM,0); if(sock < 0){ perror("socket"); exit(1); } int opt = 1;//地址复用,避免time_wait导致无法使用 setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); struct sockaddr_in server; server.sin_family = AF_INET; server.sin_port = htons(port); server.sin_addr.s_addr = inet_addr(ip); if(bind(sock,(struct sockaddr*)&server,sizeof(server)) < 0){ perror("bind"); exit(3); } if(listen(sock,5) < 0){ perror("listen"); exit(4); } return sock; } int main(int argc,char* argv[]){ if(argc != 3){ printf("Usage: [%s] [ip] [port]\n",argv[0]); return 1; } int listen_sock = startup(argv[1],atoi(argv[2])); struct sockaddr_in client; socklen_t len = sizeof(client); for(;;){ int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len); if(new_sock < 0){ perror("accept"); continue; } char buf_ip[1024]; memset(buf_ip,'\0',sizeof(buf_ip)); inet_ntop(AF_INET,&client.sin_addr,buf_ip,sizeof(buf_ip)); printf("get a new connect:[%s|%d]\n",buf_ip,ntohs(client.sin_port)); while(1){ char buf[1024]; memset(buf,'\0',sizeof(buf)); read(new_sock,buf,sizeof(buf)); printf("client: %s\n",buf); printf("server :$ "); fflush(stdout); memset(buf,'\0',sizeof(buf)); ssize_t s = read(0,buf,sizeof(buf)-1); buf[s-1] = '\0'; write(new_sock,buf,sizeof(buf)); printf("Please wait:..."); } } close(listen_sock); return 0; } |
TCP客户端
clude <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/socket.h> #include <sys/types.h> #include <string.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]); } char buf[1024]; memset(buf,'\0',sizeof(buf)); struct sockaddr_in server; int sock = socket(AF_INET,SOCK_STREAM,0); bzero(&server,sizeof(server)); server.sin_family = AF_INET; inet_pton(AF_INET,"127.0.0.1",&server.sin_addr); server.sin_port = htons(atoi(argv[2])); int ret = connect(sock,(struct sockaddr*)&server,sizeof(server)); if(ret < 0){ perror("connect"); return 2; } printf("connect success.....\n"); while(1){ printf("client:# "); fflush(stdout); ssize_t s = read(0,buf,sizeof(buf)-1); if(s > 0){ buf[s] = '\0'; write(sock,buf,strlen(buf)); memset(buf,'\0',sizeof(buf)); read(sock,buf,sizeof(buf)); printf("server: %s\n",buf); } } close(sock); return 0; } |
由于客户端不需要固定的端口号,所以不用调用bind(),客户端的端口号可以指定分配。但也不是客户端不允许使用bind();而是没有必要和固定的端口号绑在一起,否则如果在一台机器上启动多个客户端,就会出现端口号被占用的情况而无法正常建立连接。
服务器也不是一定要调用bind();但如果服务器不调用bind();内核就会自动给服务器分配监听端口,每次启动服务器的端口号都不一样,客户端需要链接服务器是就会比较麻烦。