(一)简单开发模型
a)服务器端
1 创建socket设备,返回该设备的文件描述符 sfd socket(2)
2 将sfd和本地ip地址 端口进行绑定 bind(2)
3 将sfd设置位被动连接状态,客户端连接请求到来的时候, 将这些连接请求存放到sfd设备的未决连接队列中
listen(2)
while(1){
4 从sfd设备的未决连接队列中取出一个进行连接处理,返回连接描述符. accept(2)
5> 使用连接描述符,获取客户端的请求信息 read(2)
6 处理客户端的请求信息
7 将处理结果通过连接描述符,返回给客户端write(2)
8 通过条件判断是否关闭连接描述符,结束本次通讯 close(2)
}
b)客户端
1 创建socket设备,返回该设备的文件描述符fd. socket(2)
2 使用fd向服务器发起连接 connect(2)
while (1){ 3 向服务器发送消息 write(2)
4 阻塞等待服务器的响应消息 read(2)
5 处理响应消息
6 判断是否关闭本次连接 close(2)
}
(二)系统调用
-
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建一个用于通讯的端点,并且返回该端点的描述符
参数:
domain
AF_INET IPv4 Internet protocols ip(7)
AF_INET6 IPv6 Internet protocols ipv6(7)
… …详见man手册
type
SOCK_STREAM TCP
SOCK_DGRAM UDP
… …详见man手册
protocol 通常,在给定协议族中,只有一个协议可以支持特定的套接字类型,在这种情况下,协议可以指定为0
返回值:
成功 返回一个新的文件描述符 用于socket设备
失败 -1 errno被设置 -
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:将name绑定到socket设备上
参数:
sockfd 指定了具体的socket设备
addr 指定了具体的地址,该参数是一个结构体指针,实参应该为一个指向具体的地址类型的结构体的指针被强转后的指针。如实参p为一个指向struct sockaddr_in的一个指针,然后强转成struct sockaddr类型的指针
addrlen 指定了addr地址结构体的大小
返回值:
成功 0
错误 -1 errno被设置struct sockaddr{ sa_family_t sa_family; char sa_data[14]; };
-
listen(2)
int listen(int sockfd, int backlog);
功能:将sockfd指定的设备设置为被动连接状态.将客户端到来的连接放入未决连接队列中.
参数:
sockfd 指定了具体的socket设备
backlog 指定了设备上的未决连接队列的最大数
返回值:
成功 0
错误 -1 errno被设置 -
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能: 接收一个连接,在指定的socket设备上
在指定的socket设备上,提取第一个未决连接请求,创建一个连接描述符,返回该描述符.
参数:
sockfd 指定socket设备
addr 将客户端的地址存放到addr指定的地址空间里
如果addr为NULL,addrlen也设置为NULL.
addrlen 指定了addr空间的大小,返回实际接收到的字节数
返回值:
成功 返回一个新的文件描述符.连接描述符
失败 -1 errno被设置 -
connect(2)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:在指定的socket设备上发起连接
参数:
sockfd 指定具体的socket设备
addr 指定目标服务器的地址
addrlen 指定addr空间的大小
返回值:
成功 0
错误 -1 errno被设置 -
setsockopt
功能:设置socket选项,详见man手册,server.c中有简单应用
(三)其他工具函数
- #include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
功能:convert IPv4 and IPv6 addresses from text to binary form
参数:
af: AF_INET ipv4 AF_INET6 ipv6
src:指向待转换的字符串“xxx.xxx.xxx.xxx”
dst:如果是ipv4,指向一个struct in_addr的结构体,该结构体是struct sockaddr_in 的一个成员;该结构体内只有一个成员,是32位无符号整型,即unsinged int(uint32_t);该结构体内存的是网络字节序的ip地址;
返回值:1成功 0 src不是有效的 -1af无效,errno被设置 - #include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
功能:convert IPv4 and IPv6 addresses from binary to text form
参数:
af 同上inet_pton
src 如果是ipv4, 指向一个struct in_addr, 该结构体存的是二进制形式的网络字节序的ip地址
dst 非空指针,用来指向存储转换结果的空间
size 指定dst空间的大小,至少为INET_ADDRSTRLEN(man手册,但是没找到这个宏)
返回值:成功返回dst,失败返回NULL,errno被设置 - #include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
功能:32位或12位无符号数,在网络字节序和主机字节序之间的转换。详见man手册
(四)代码
t_stdio.h
#ifndef __T_STDIO_H__
#define __T_STDIO_H__
#include <stdio.h>
#define E_MSG(STR,VAL) do{\
perror(STR);\
return (VAL);\
}while(0)
#endif
server.c
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <t_stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <string.h> #include <signal.h> #include <sys/wait.h> void do_wait(int n){ wait(NULL); return; } int main(void){ signal(SIGCHLD, do_wait); char buf[128]; struct sockaddr_in serv;//该类型查看 man 7 ip 获得详情 //创建socket设备,返回该设备的文件描述符 int sfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == sfd) E_MSG("socket", -1); //设置选项,地址重用,详见man手册 int optval = 1;//非0代表真 //SO_REUSEADDR代表地址重用,详见socket(7) int setopt = setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)); if(-1 == setopt) E_MSG("setsockopt", -1); //初始化serv serv.sin_family = AF_INET; serv.sin_port = htons((uint16_t)6666);//将6666从主机序转换成网络序,赋值给sin_port serv.sin_addr.s_addr = htonl(INADDR_ANY); //将INADDR_ANY转成网络序(本质不转也行,因为它是0.0.0.0的二进制),代表监听本主机的所有ip,访问本主机的任一ip的6666端口,都将访问到本进程 //将本地地址(ip地址和端口)绑定到socket设备sfd int b = bind(sfd, (const struct sockaddr *)&serv, sizeof(serv)); if(-1 == b) E_MSG("bind", -1); //将sfd设置为被动连接状态。监听客户端连接的请求,将客户端到来的连接请求放入到未决连接队列中 int lis = listen(sfd, 5); if(-1 == lis) E_MSG("listen", -1); while(1){ //从sfd指定的设备的未决连接队列中取出一个连接请求,进行连接处理,返回和客户端的连接描述符 int cfd = accept(sfd, NULL, NULL); if(-1 == cfd) E_MSG("accept", -1); int pid = fork(); if(0 == pid){ //子进程 close(sfd); while(1){ //读取客户端请求 int r = read(cfd,buf,128); if(-1 == r){ printf("hahahhahah");E_MSG("read", -1);} //处理客户端的请求信息 for(int i=0; i<r; i++) if(buf[i] >= 'a' && buf[i] <= 'z') buf[i] &= ~32; //将处理结果返回给客户端 int err = write(cfd, buf, r); if(-1 == err) E_MSG("write", -1); //关闭本次连接 if(!strcmp(buf, "BYEBYE")){ printf("close a cli\n"); err = close(cfd); if(-1 == err) E_MSG("close", -1); break; } } break;//退出子进程 }else{ //父进程 close(cfd); } } return 0; }
client.c
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <t_stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <string.h> int main(int argc, char *argv[]){ char buf[128]; struct sockaddr_in cli;//该类型查看 man 7 ip 获得详情 //创建socket设备,返回该设备的文件描述符 int cfd = socket(AF_INET, SOCK_STREAM, 0); if(-1 == cfd) E_MSG("socket", -1); //初始化cli cli.sin_family = AF_INET; cli.sin_port = htons((uint16_t)6666);//将6666从主机序转换成网络序,赋值给sin_port //用inet_pton将字符串转换成网络字节序的二进制ip inet_pton(AF_INET, argv[1], &cli.sin_addr); int con = connect(cfd, (const struct sockaddr *)&cli, sizeof(cli)); if(-1 == con) E_MSG("connect", -1); while(1){ //从终端得到一个字符串 fgets(buf, sizeof(buf), stdin); if(strlen(buf)==sizeof(buf)-1 && buf[strlen(buf)-1] != '\n'){ scanf("%*[^\n]"); scanf("%*c"); }else buf[strlen(buf)-1] = '\0'; //发送给服务器 int w = write(cfd, buf, strlen(buf)+1); if(-1 == w) E_MSG("write", -1); //接收服务器处理后的信息 int r = read(cfd, buf, sizeof(buf)); if(-1 == r) E_MSG("read", -1); //输出到屏幕 write(1, buf, r); printf("\n"); if(!strcmp(buf, "BYEBYE")){ close(cfd); break; } } printf("quit!!!\n"); return 0; }
编译:gcc server.c -o serv
gcc client.c -o cli
运行效果: 先运行服务器 serv,再新开shell运行客户端cli,在客户端中输入任意字符串,都将被服务器转换成大写,直到客户端输入byebye断开连接。可开多个客户端,服务器通过多进程实现并发。