摘要
随着社会网络化的发展,互联网对人们的生活方式产生极大的影响,同时,也创造了一批互联网企业,如著名的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客户/服务器程序示例-----------------------------------------------------------------------------
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);
}
}
}
TCP客户端程序
/* ==========================================================
客户端程序
==========================================================
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);
}
Makefile 文件
#---------------------------------------
# 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
自动化配置Shell脚本
#! /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