随着社会网络化的发展,互联网对人们的生活方式产生极大的影响,同时,也创造了一批互联网企业,如著名的BAT。作为一个IT程序员,学会网络通信编程显得十分重要,本文将详细讲解网络编程API之一的套接字编程基本知识,同时充分利用Linux环境下的 Shell脚本和Makefile文件功能,实现一个简单智能化的安装配置。
资源下载
----------------------------------------------------------------------------------套接字地址结构-------------------------------------------------------------------------------------------------
一些概念:
端口:16位整数,TCP,UDP和SCTP用于区分不同的进程,分为众所周知的端口(1-1023),已登记的端口(1024-49151)和临时端口(49152-65535)
套接字:标识每个端点的两个值(IP地址和端口号),通常称为一个套接字。
TCP套接字对:定义连接的两个端点四元组:本地地址IP,本地TCP端口号,外地IP地址,外地TCP端口号。套接字对唯一标识一个网络上的每个TCP连接
套接字地址结构定义在 <netinet/in.h> 头文件中 IPv4 结构: struct in_addr { in_addr_t s_addr; // 32-bit IPv4 address, 网络字节序 }; struct sockaddr_in { uint8_t sin_len; // 结构长度(16B),若无此项则 协议族为2B sa_family_t sin_family;// AF_INET in_port_t sin_port; //16-bit 端口号 struct in_addr sin_addr; //32- bit IPv4 地址 char sin_zeros[8];// 未使用 } 通用套接字地址结构(<sys/socket.h>)用于处理不同协议族的地址结构引用传递: struct sockaddr { uint8_t sa_len; sa_family_t sa_family; // 地址族:AF_XXX value char sa_data[14]; }; IPv6 结构: struct in6_addr { in_addr_t s6_addr[16]; // 128-bit IPv6 address, 网络字节序 }; #define SIN6_LEN struct sockaddr_in6 { uint8_t sin6_len; // 结构长度(28B) sa_family_t sin6_family;// AF_INET6 in_port_t sin6_port; //16-bit 端口号 uint32_t sin6_flowinfo; //未定义 struct in6_addr sin6_addr; //128- bit IPv4 uint32_t sin6_scope_id;// 范围地址 } IPv6通用套接字地址结构(<netinet/in.h>)可容纳系统支持的任何套接字地址结构: struct sockaddr_storage { uint8_t sa_len; sa_family_t sa_family; // 地址族:AF_XXX value /* … */ };
--------------------------------------------------------------------------基本套接字函数使用介绍---------------------------------------------------------------------------------------
基本的TCP套接字编程函数包括 socket,connect,bind,listen,accept等5个函数,需包含头文件 #include <sys/socket.h>;还有提供服务器并发功能的函数 fork和 exec函数族,和close函数(用于关闭套接字),包含头文件 #include <unistd.h>
1 socket函数
函数说明:执行网络I/O,一个进程必须做的一件事即调用socket函数,指定期望的通信协议类型
函数定义:int socket(int family, int type, int protocol);
三个参数分别为协议族,套接字类型,协议类型常值(通常设置为0)
IPv4协议用法:sockfd =socket(AF_INET,SOCK_STREAM,0);
成功则返回非负描述符,即套接字描述符 sockfd,出错返回 -1
2 connect函数
函数说明:基于套接字描述符sockfd的函数,用来建立TCP服务器的连接,调用将激发TCP的三路握手过程,仅在连接建立成功或出错时才返回
函数定义:int connect(int sockfd, conststruct sockaddr *servaddr,socklen_t addrlen);
套接字描述符,套接字地址结构指针和该结构大小
IPv4协议用法:if(connect(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr)) <0) exit(1);
成功返回0,出错返回-1
3 bind函数
函数说明:基于套接字描述符sockfd的函数,用来把一个本地协议地址赋予一个套接字,如果不调用bind函数,内核则为相应的套接字选择一个临时端口,但对于服务器来说,需要用bind来绑定一个众所周知的端口,这是很必要的。
函数定义:int bind(intsockfd, const struct sockaddr *myaddr, socklen_t addrlen);
套接字描述符,指向特定于协议的地址结构指针和该地址结构长度
IPv4协议用法:if(bind(sockfd,(struct sockaddr *) &servaddr, sizeof(servaddr))<0) exit(1);
成功返回0,出错返回-1
4 listen函数
函数说明:仅由TCP服务器调用,在服务器端,socket创建一个套接字时,被假设为一个主动套接字,即一个将调用connect发起连接客户套接字,listen函数把一个未连接的套接字转换成一个被动套接字(等待客户请求),指示内核应接受指向该套接字的连接请求。通常在调用socket,bind函数之后,并在调用accept函数之前调用。
函数定义:int listen(int sockfd, int backlog) ;
第二个参数规定了内核应该为相应套接字排队的最大连接个数。监听套接字维护两个队列:未完成连接队列(SYN_RCVD)和已完成连接队列(ESTABLISHED),未完成连接队列成员经过三路握手成功后可进入已完成连接队列。
IPv4协议用法:if(listen(sockfd,LISTEN))exit(1);
成功返回0,出错返回-1
5 accept函数
函数说明:由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接,如果已完成连接队列为空,则进程被投入睡眠状态(阻塞)
函数定义:int accpt(intsockfd, struct sockaddr *cliaddr,socklen_t *addrlen);
第一个参数即套接字描述符称为监听套接字(一般用listenfd代替sockfd),剩下俩个为已连接的对端客户进程的协议地址和长度,第三个参数为值-结果参数
IPv4协议用法:connfd=accept(listenfd, (struct sockaddr*) &cliaddr, &len);;
若成功返回非负描述符,称之为已连接套接字(常用connfd表示),出错则返回-1
注-监听套接字和已连接套接字的区别:一个服务器通常仅创建一个监听套接字,并且在服务器周期内一直存在,内核为每个由服务器进程接受的客户连接创建一个已连接套接字(即TCP三路握手过程已经完成),当服务器完成对给定客户服务时,相应的已连接套接字关闭。
6 fork函数和exec函数
函数说明:调用一次,返回两次,即父进程和子进程,根据返回值判断当前进程是父进程还是子进程。exec函数通常被fork子进程调用,然后将子进程替换成新的程序,被称为调用进程,具体可参见我之前总结的一篇对这两个函数分析的文章:
http://blog.csdn.net/gujinjinseu/article/details/25838381
函数定义:pid_t fork(void);
7 close函数
函数说明:用于关闭套接字,并终止TCP连接
函数定义:int close(int sockfd);
成功返回0,出错返回-1
注: 与shutdown 函数的区别,close 关闭可能是多连接的套接字,此时并不关闭fd,而shutdown则直接关闭fd (file descriptor)
------------------------------------------------------------------------------------TCP客户/服务器程序示例-----------------------------------------------------------------------------
/* ========================================================== 服务器端程序 ========================================================== By gujj 20140530 */ #include <stdio.h> // 标准输入输出流 #include <netinet/in.h> // socket struct #include <sys/socket.h> #include <time.h> // time #include <string.h> // htons() #include <stdlib.h> // exit() #include <unistd.h> //write() #define MAXLINE 4096 #define LISTENQ 1024 // 监听套接字最大连接数 int main(int argc, char **argv) { // 声明定义监听套接字,已连接套接字 int listenfd, connfd,n; // 被动套接字 struct sockaddr_in servaddr; char buff[MAXLINE]; time_t ticks; /* 监听套接字函数 */ if(( listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0 ) { printf("socket error! \n"); exit(1); } /* 初始化服务器端套接字地址结构 */ memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); /* 任意地址通配 */ servaddr.sin_port = htons(13); /* daytime 服务 */ /* bind 函数 */ if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 ) { printf("bind error! \n"); exit(1); } /* listen 函数 */ // 将套接字变成 内核可以接受的形式 if( listen(listenfd, LISTENQ) < 0 ) { printf( "listen error! \n" ); exit(1); } /* 循环处理客户请求并反馈 */ for( ; ; ) { if((connfd = accept(listenfd,(struct sockaddr *)NULL,NULL))<0) { printf("accept error! \n"); exit(1); } ticks = time(NULL); snprintf(buff, sizeof(buff), "%24s\r\n", ctime(&ticks)); if((n = write(connfd,buff,strlen(buff)))<0) { printf("write error! \n"); exit(1); } if(close(connfd)<0) { printf("accept error! \n"); exit(1); } } }
/* ========================================================== 客户端程序 ========================================================== By gujj 20140530 */ #include <stdio.h> // 标准输入输出流 #include <netinet/in.h> // socket struct #include <sys/socket.h> #include <arpa/inet.h> // inet_pton() #include <string.h> // htons() #include <stdlib.h> // exit() #include <unistd.h> // read() #define MAXLINE 4096 int main(int argc, char **argv) { int sockfd,n; char recvline[MAXLINE + 1]; // 最后一个结束符 struct sockaddr_in servaddr; /* 参数个数判断 */ if(argc != 2) { printf("Usage: a.out <IPaddress> \n"); exit(1); } /* 套接字函数调用 */ if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { printf("socket error! \n"); exit(1); } /* 初始化服务器端套接字地址结构 */ memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(13); /* inet_pton 点分十进制到网络字节序二进制地址序列转换 */ // =0 表示输入地址错误, <0 表示出错 if(inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { printf("inet_pton error for %s \n", argv[1]); exit(1); } /* connect 函数 */ if(connect(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))<0) { printf("connect error! \n"); exit(1); } /* read 函数 */ // n=0 关闭,n>0 读取字节数, <0 失败 while(( n = read(sockfd, recvline, MAXLINE)) > 0 ) { recvline[n] = 0; // 最后一个字节 添加 NULL if(fputs(recvline,stdout) == EOF) { printf("fputs error \n"); exit(1); } } if(n<0) { printf("read error \n"); exit(1); } exit(0); }
#--------------------------------------- # Generate Server and Client for TCP #--------------------------------------- # By Gu Jinjin # multi-target all: client server client: tcpclient.o gcc -o client tcpclient.o server: tcpserver.o gcc -o server tcpserver.o # compile tcpclient.o: tcpclient.c gcc -c -g tcpclient.c tcpserver.o: tcpserver.c gcc -c -g tcpserver.c # define the pseudo-target .PHONY : cleanall clean cleanall: clean rm -rf client server clean: rm -rf tcpclient.o tcpserver.o
#! /bin/bash # By Gu Jinjin # Install client & server for TCP # Debug or Test function # Uninstall # Note: if you edit in NotePad++, please use UTF-8 without BOM style if [ $# -ne 1 ];then echo "\t------------------------------------" echo "\tUsage: sh setup <args>" echo "\t args are Numbers as follows:" echo "\t 1. make, generate executions" echo "\t 2. run, make & run" echo "\t 3. make clean, rm *.o" echo "\t 4. make cleanall, rm files in 1&2" echo "\t----------------------" echo "\t Exp: sh setup.sh 2 " echo "\t----------------------" echo "\t------------------------------------" exit 1 fi case $1 in 1) make > info.make 2>&1 ;; 2) make > info.make 2>&1 make clean path=`echo $PWD` sudo $PWD/server & #$PWD/client 127.0.0.1 ;; 3) make clean ;; 4) sudo pkill server make cleanall rm -rf info.make ;; *) echo "\tInput Wrong Args, please use command: " echo "\t\tsh setup.sh" echo "\tfor more Info!" ;; esac exit 0
编写脚本实现工程的编译连接安装并运行,是Linux系统下的一大特点和优势,本文实现一个简单的智能化脚本,提供基于TCP协议的客户端服务器端运行测试,以及删除选项,具体参见下图说明,这里解释一下: 1 即make为运行make命令编译 client 和server, 2. 即包括1中内容和运行服务器端,在后台运行, 3. 删除编译产生的 .o文件,4. 终止服务器端后台运行,并删除所有文件
UNIX网络编程卷1(第三版)
高级Bash脚本编程指南(Appendix Q, 杨春敏,黄毅 译)
Makefile:http://www.chinaunix.net/old_jh/23/408225.html