网络通信的过程:
服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。客户端依次调用socket()、connect()之后就向服务器发送了一个连接请求。服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了
1.socket函数:创建套接字
函数原型为:
int socket(int domain, int type, int protocol);
1.第一个参数为协议族,常用AF_INET,internet协议族
2.套接字类型,常用的有三种:
SOCK_STREAM TCP套接字,按顺序发送(四层结构)
SOCK_DGRAM UDP套接字,按顺序发送
SOCK_RAW 原始套接字,发送的时候穿透了传输层
3.成功返回一个文件描述符,失败返回-1
2.bind函数:把地址和端口号的组合赋给socket
函数原型为:
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
1.第一个参数为 函数返回的文件描述符
2.第二个参数用来保存ip地址和端口号
IP地址(IPV4)占有4个字节,端口号占有2个字节,因此定义了一个结构体struct sockaddr来存储, 这个
结构体含有两个成员,第一个成员占据两个字节的大小,存放协议族,一般使用Internet,即AF_INET。第二
个成员也是一个结构体,占据14个字节的大小,存放ip地址和端口号,但是初始化不方便,因此采用另一种结
构体struct sockaddr_in.
struct sockaddr_in
{
u_short sin_family; // 地址族, AF_INET,2 bytes
u_short sin_port; // 端口,2 bytes 0---65536
struct in_addr sin_addr; // IPV4地址,4 bytes
char sin_zero[8]; // 8 bytes unused,作为填充
};
该结构体含有四个成员,分别对应以上的三个元素和另外的8个字节的补充单元。初始化ip地址时用inet_addr
函数转换成无符号长整型。但是传参数的时候需要强制类型转换成struct sockaddr*类型的
3.第二个参数占用空间的大小,strlen(struct sockaddr)
4.函数成功执行返回0,失败返回-1
3.listen函数:服务器端的监听函数,监听socket,如果客户端调用connect发出链接请求,服务器就会接收到
函数原型:
int listen(int sockfd, int backlog)。
1.socket函数返回的文件描述符
2.设置客户端请求的队列长度,大多数系统允许数目为20
3.函数成功执行返回0,失败返回-1
4.accept函数:接收客户端的请求
函数原型为:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
1.第一个参数为socket函数返回的文件描述符
2.第二个才参数包含客户端的ip地址和端口号,不需要可以设置为NULL
3.描述第二个参数的大小,不需要可以设置为NULL
5.区分监听套接字和链接套接字
监听套接字只是负责监听是否有请求,当客户端发来请求以后,调用accpet函数,并返回一个链接套接字,此时与客户端的通信完全是由这个链接套接字来完成,而监听套接字依旧在监听。
6.connect函数:客户端通过调用connect函数向服务器端请求连接
函数原型为:
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
与bind函数的参数完全一致,一个用在服务器端一个用在客户端
1.第一个参数:客户端的socket描述符
2.第二个参数:服务器端的socket地址,内涵ip和端口号
3.第三个长度:socket地址的长度
7.IP地址的转换
主机字节序到网络字节序:
1.inet_pton(AF_INET,"ip地址",(void *)&var.sin_addr);//成功返回1,适用于ipv4和ipv6
2.var.sin_addr.s_addr = inet_addr("ip地址");
3.var.sin_addr.s_addr = htonl(INADDY_ANY);//可以绑定任意的ip,只要端口号正确,套接字类型正确
就都可以处理
网络字节序到主机字节序:
1.inet_ntop(AF_INET,(void *)&var2.sin_addr,buf,sizeof(var2));//失败返回NULL,使用ipv4和ipv6
8.端口号的转换:
主机字节序到网络字节序:
htons(端口号);//自行分配的端口号最好在5000以后
网络字节序到主机字节序:
ntohs(端口号);
还有两个函数htonl和ntohl用于32位数的转换。
9.程序实现:C\S模式下的互发消息
//server
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERV_PORT 5001
#define BACKLOG 5
void exitfun(int sig);
void *sendfun(void *arg);
int main(int argc, char *argv[])
{
int fd;
int rw_fd;
pthread_t tid;
int ret;
pid_t pid;
socklen_t len = sizeof(struct sockaddr);
struct sockaddr_in sin = {0};
struct sockaddr_in cin = {0};
char buf[32] = {0};
//创建socket
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket");
exit(-1);
}
//初始化并绑定本机的ip和端口
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);
sin.sin_addr.s_addr = htonl(INADDR_ANY);
if((bind(fd, (struct sockaddr *)&sin, sizeof(sin))) < 0)
{
perror("bind");
exit(-1);
}
//监听socket
if(listen(fd, BACKLOG) < 0)
{
perror("listen");
exit(-1);
}
signal(10,exitfun); //接受子进程或者线程发来的信号,当通信结束后结束进程并回收资源
while(1) //循环调用,每当有客户端发送链接请求就产生一个子进程去与之通信,主进程继续监听
{
if((rw_fd = accept(fd, (struct sockaddr *)&cin, &len)) < 0) //接收客户端请求
{
perror("accept");
exit(-1);
}
if((inet_ntop(AF_INET, (void *)&cin.sin_addr, buf, sizeof(cin))) == NULL) //转化客户端的ip地址
{
perror("inet_ntop");
exit(-1);
}
printf("Cilent (%s:%d) is connected!\n", buf, ntohs(cin.sin_port)); //打印客户端ip和端口号
pid = fork();
if(pid == 0)
{
close(fd);
if((ret = pthread_create(&tid, NULL, sendfun, (void *)&rw_fd)) == -1) //子线程负责发送消息
{
perror("pthread_create");
exit(-1);
}
while(1) //主线程负责接收消息
{
memset(buf, 0, 32);
read(rw_fd, buf, 32);
if(strcmp(buf, "quit") == 0)
{
close(rw_fd);
kill(getppid(), 10);
exit(-1); //线程推出,进程也就退出了
}
printf("receive from cilent = %s\n",buf);
}
}
if(pid > 0)
{
close(rw_fd); //只有父子进程读关闭文件,这个文件才会真正被关闭,一方关闭不影响另一方使用
}
}
close(fd);
return 0;
}
void exitfun(int sig)
{
printf("cilent exit\n");
wait(NULL);
}
void *sendfun(void *arg)
{
char buf[32] = {0};
int rw_fd;
rw_fd = *((int *)arg);
while(1)
{
scanf("%s",buf);
write(rw_fd, buf, strlen(buf));
if(strcmp(buf, "quit") == 0)
{
close(rw_fd);
kill(getppid(),10); //客户端发送quit,断开链接,发送信号给父进程,回收资源
exit(-1); //子线程中使用exit进程也会结束
}
}
}
//cilent
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERV_PORT 5001
#define BACKLOG 5
void *readfun(void *arg);
int main(int argc, char *argv[])
{
int fd;
pthread_t tid;
int ret;
struct sockaddr_in sin = {0};
char buf[32] = {0};
//创建socket
if((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket");
exit(-1);
}
//初始化并绑定本机的ip和端口
sin.sin_family = AF_INET;
sin.sin_port = htons(SERV_PORT);
sin.sin_addr.s_addr = inet_addr("192.168.30.170");
if((connect(fd, (struct sockaddr *)&sin, sizeof(sin))) < 0)
{
perror("bind");
exit(-1);
}
if((ret = pthread_create(&tid, NULL, readfun, (void *)&fd)) == -1) //子线程负责发送消息
{
perror("pthread_create");
exit(-1);
}
while(1) //主线程负责发送消息
{
scanf("%s", buf);
write(fd, buf, strlen(buf));
if(strcmp(buf, "quit") == 0)
{
break;
}
memset(buf, 0, 32);
}
return 0;
}
void *readfun(void *arg) //子线程负责接收消息
{
char buf[32] = {0};
int fd;
fd = *((int *)arg);
while(1)
{
read(fd, buf, 32);
if(strcmp(buf, "quit") == 0)
{
exit(-1);
}
printf("receive from server = %s\n", buf);
memset(buf, 0, 32);
}
}
10.关于文件的关闭
父子进程中如果有共享的文件描述符,如果用不到就在开头直接关闭(并不会影响另一个的使用),并且需要用的文件描述符也要在用完后关闭,相当于每个文件必须关闭两次
11.常见问题
1.打印客户端地址为0:accept函数的最后一个参数在定义时需要设置初值,大小为16,或者sizeof(struct sockaddr_in);
https://www.cnblogs.com/jiangzhaowei/p/8261174.html