(前期使用Ubuntu18.04,后期换成了Deepin20,但是二者都是Debian系的所以各种操作不耽误)
- 什么是Socket
英语socket是插座,插孔的意思,中文译作套接字;
插销和插座插在一起,就能通电,引申得到两个socket套接在一起就可以通信;
既然是插座和插销,所以socket一定是成对出现的;
IP地址:在网络环境中唯一标识一台主机;
Port:在主机中唯一标识一个进程;
IP地址+Port:在网络环境中唯一标识一个进程,这个进程就是Socket;
Socket是Linux的一种文件类型(伪文件,不会占用实际的存储空间;Linux七种文件类型:占存储空间:普通文件、目录、软连接,不占存储空间:字符设备、块设备、管道、套接字);
Socket是全双工的,一个文件描述符对应两个缓冲区,分别进行读和写:
- 网络字节序转化
数据在网络中传输就要转化成二进制形式;
主机字节序是小端法,高位存在高地址,低位存在低地址;网络字节序是大端法,高位存在低地址,低位存在高地址:(int类型四个字节,每个字节八位,而一个十六进制数四位,所以一个字节存两个十六进制数)
网络字节序和主机字节序转化的函数:
uint32_t htonl(uint32_t hostlong)//uint32_t:32位无符号整型;htonl:host to network long,32位数值从主机字节序转为网络字节序;主机发送数据到网络环境时用;
uint16_t htons(uint16_t hostshort)//htons:host to network short,16位数值从主机字节序转为网络字节序;
uint32_t ntohl(uint32_t netlong)//32位数值从网络字节序转为主机字节序;主机从网络环境接收数据时用;
uint16_t ntohl(uint16_t netshort)
- 专门用于IP地址转化的函数
#include <arpa/inet.h>
int inet_pton(int af,const char *src,void *dst)//将IP地址由点分十进制转为网络字节序;af是地址族,有两个值AF_INET表示IPv4,AF_INET6表示IPv6;src是待转换的点分十进制IP字符串,const表示只读;dst是转换后的IP;
int inet_ntop(int af,const void *src,char *dst,socklen_t size)//将IP地址由网络字节序转为点分十进制字符串;af地址族;src待转换的网络字节序IP;dst转换后的点分十进制字符串(首地址);size是点分十进制字符串长度;
htonl等函数操作的是数值类型,inet_pton操作的是字符串;
- sockaddr数据结构
最初的struct sockaddr是一个描述IPv4的结构体,包括地址类型(16bit)和地址数据(32Byte);
后来经过改进,改为struct sockaddr_in,包括地址类型=AF_INET(16bit)、端口号(16bit)、IP地址(32bit)和填充(8bit),大小没变,只是成员细分了;
struct sockaddr_in成员:
成员1是协议族,成员2是端口号,都是typedef的数据类型;
成员3是IP地址,又是一个结构体,只有一个成员:
现在只有struct sockaddr_in,struct sockaddr已经废弃了,但是现在的函数传参还是struct sockaddr类型,比如bind()函数有一参struct sockaddr *addr,现在只能先定义struct sockaddr_in addr,再传参(struct sockaddr *)&addr,因此在程序中看到这种强制类型转换要知道是什么意思,这是历史遗留问题,只能通过强转解决;
- socket()函数
参数int type:SOCK_STREAM表示“流式”传输,SOCK_DGRAM表示“报式”传输;
参数int protocol:传0表示使用该type的默认协议,流式默认协议是TCP,报式默认协议是UDP;
一般参数int domain可取两个值AF_INET和AF_INET6,参数int type可取两个值SOCK_STREAM和SOCK_DGRAM,参数int protocol取0;
返回值:成功,返回新创建的socket文件的文件描述符;失败,返回-1;
PS:什么是文件描述符:文件IO操作中,调用open()函数会得到一个结构体,其中包含了操作这个文件所需要的所有文件属性,指向这个结构体的指针被保存在当前进程空间中的一个数组里,结构体指针所在的数组下标就是文件描述符(fd);因此文件描述符是个int正整数;fopen()函数依赖于open()函数,而调用fopen函数会得到一个File *型指针,指向的也是一个结构体,fd是这个结构体的一个成员;
- bind()函数
- listen()函数
指定socket可以同时建立多少个连接,如果参数int backlog为100,并且此时已经建立了100个连接,那么第101个来了就得等着直到前面100个有断开连接的;
操作系统限定最大是128;
- accept()函数
参数2是个传出参数,只需要定义一个(struct sockaddr)struct sockaddr_in addr,不需要初始化直接取地址传进去,经过函数调用后addr已经完成初始化了,是一个新的socket的sockaddr结构体;返回值也是一个新socket的fd;
作用:服务端调用accept()函数后阻塞,直到收到来自客户端的连接,这时创建一个新的socket负责与客户端通信,而服务端继续阻塞等待下一个客户端的连接;
也就是说accept()函数是只在服务端调用的;
- connect()函数
用于连接服务器,参数看起来和bind()函数一样,但是这里参数2是初始化好的对方socket的sockaddr结构体、3是结构体大小;
只在客户端调用;
- 服务端编程
vim server.c
#include <stdio.h>
#include <stdlib.h>//包含exit()
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>//包含struct sockaddr_in
#include <ctype.h>//包含toupper()
#include <strings.h>//包含bzero()
#define SERV_IP INADDR_ANY //INADDR_ANY是192.168.x.x内部地址
#define SERV_PORT 8888
int main(void)
{
int serv_fd, clie_fd;
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
char buf[BUFSIZ], clie_ip_buf[BUFSIZ];//BUFSIZ是内置的宏,专门用来指定buf大小
int n, i;
serv_fd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));//类似于memeset(),bzero()是直接初始化为0
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);//host to network short转成网络字节序
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//host to network long转成网络字节序;
bind(serv_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(serv_fd, 128);
clie_addr_len = sizeof(clie_addr);
clie_fd = accept(serv_fd, (struct sockaddr *)&clie_addr, &clie_addr_len);//参数3是传入传出参数,因为传入,所以初始化,因为传出,所以取地址
printf("client IP: %s, client port: %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip_buf, sizeof(clie_ip_buf)), ntohs(clie_addr.sin_port));
while(1){
n = read(clie_fd, buf, sizeof(buf));//从客户socket里读,读到buf里,读的空间是sizeof(buf)这么大,读到了n个字符
for(i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
write(clie_fd, buf, n);//往clie_fd里写,写的是buf里的东西,写n个字符;
}
close(serv_fd);
close(clie_fd);
return 0;
}
:wq
gcc server.c -o server
./server
- 客户端编程
vim client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>//包含struct sockaddr_in
#include <string.h>//包含memset()
#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888
int main(void)
{
int clie_fd;
struct sockaddr_in serv_addr, clie_addr;
char buf[BUFSIZ];//BUFSIZ是内置的宏,专门用来指定buf大小
int n;
clie_fd = socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));//把这一块内存空间全部清成0;memset()用来为新申请的内存做初始化
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);//host to network short转成网络字节序
inet_pton(clie_fd, SERV_IP, &serv_addr.sin_addr.s_addr);//IP字符串转成网络字节序;
connect(clie_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
while(1){
fgets(buf, sizeof(buf), stdin);//参数1:目的缓冲区指针,参数2:缓冲区大小,参数3:源数据流;stdin:标准输入
write(clie_fd, buf, strlen(buf));//键盘输入hello/n,fgets后buf里的是hello/n/0,strlen(buf)取的是/0之前的长度
n = read(clie_fd, buf, sizeof(buf));//写完之后发给服务端了,服务端会响应回来,现在该读了
write(STDOUT_FILENO, buf, n);//把读到的服务器的响应写到屏幕
}
close(clie_fd);
return 0;
}
:wq
gcc client.c -o client
./client
客户端发送小写字符串,服务端返回大写字符串;
- 阻塞
程序运行后并不会疯狂地无限循环,因为accept()、read()、write()等函数都是阻塞的,只有客户端连接、发送数据才会推动循环的进行;
- 查看C函数帮助文档:以socket()函数为例:
:!man socket # 在Vim中
man socket # 在终端中
- Socket工作原理
serv_fd通过accept()返回的是一个和客户端相同sockaddr的socket,对它读写就相当于对客户端读写;
两个clie_fd通过相同的IP和port能够通信;
服务端clie_fd调用write(),将内容写到写缓冲区,在网络的作用下,也写到了客户端的读缓冲区,这时客户端的read()函数就解除阻塞并读到内容;客户端clie_fd调用write()也是同理;
一个socket有两个缓冲区,每个缓冲区都是根据管道的特性打造的,即一端只能读,一端只能写;两个管道实现了socket的全双工通信;
上面提到的缓冲区和char buf [ ]缓冲区是不同的,前者位于内核区,后者位于用户区stack堆,一点关系都没有;
void exit(int status)来自stdlib.h作用是结束当前进程,退出程序;参数是0表示正常退出,不是0都表示异常退出;
- 如果Ctrl+C先关闭server再关闭client的话,下次再启动client发消息会闪退:
因为在客户端连接的情况下Ctrl+C关闭服务端,会使服务端进程处于TIME_WAIT状态,不会真正的关闭,依然占用着端口号,所以下次启动会出错;如果我们写了错误处理代码,就会报错;
查看网络进程状态:以8888端口号为例:
netstat -apn | grep 8888
- 为了方便知道出错的原因,要补充错误处理代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>//包含struct sockaddr_in
#include <ctype.h>//包含toupper()
#include <strings.h>//包含bzero()
#define SERV_IP INADDR_ANY //INADDR_ANY是192.168.x.x内部地址
#define SERV_PORT 8888
int main(void)
{
int serv_fd, clie_fd;
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
char buf[BUFSIZ], clie_ip_buf[BUFSIZ];//BUFSIZ是内置的宏,专门用来指定buf大小
int n, i;
serv_fd = socket(AF_INET, SOCK_STREAM, 0);
if(serv_fd == -1)
{
perror("socket error");
exit(1);
}
bzero(&serv_addr, sizeof(serv_addr));//类似于memeset(),bzero()是直接初始化为0
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);//host to network short转成网络字节序
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//host to network long转成网络字节序;
if(bind(serv_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
{
perror("bind error");
exit(1);
}
if(listen(serv_fd, 128) == -1)
{
perror("listen error");
exit(1);
}
clie_addr_len = sizeof(clie_addr);
clie_fd = accept(serv_fd, (struct sockaddr *)&clie_addr, &clie_addr_len);//参数3是传入传出参数,因为传入,所以初始化,因为传出,所以取地址
if(clie_fd == -1)
{
perror("accept error");
exit(1);
}
printf("client IP: %s, client port: %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip_buf, sizeof(clie_ip_buf)), ntohs(clie_addr.sin_port));
while(1){
n = read(clie_fd, buf, sizeof(buf));//从客户socket里读,读到buf里,读的空间是sizeof(buf)这么大,读到了n个字符
for(i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
write(clie_fd, buf, n);//往clie_fd里写,写的是buf里的东西,写n个字符;
}
close(serv_fd);
close(clie_fd);
return 0;
}
- 添加错误处理使代码冗杂,改用自定义函数、封装容错模块、多文件联合编译:
wrap.c(包含了所有自定义容错函数):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <errno.h>
void perr_exit(const char *s)
{
perror(s);
exit(-1);
}
int Socket(int domain, int type, int protocol)
{
int fd;
if((fd = socket(domain, type, protocol)) < 0)
perr_exit("socket error");
return fd;
}
int Accept(int fd, struct sockaddr *addr, socklen_t *len){
int new_fd;
again:
if((new_fd = accept(fd, addr, len)) < 0)
{
if((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
perr_exit("accept error");
}
return new_fd;
}
int Bind(int fd, const struct sockaddr *addr, socklen_t len)
{
int result;
if((result = bind(fd, addr, len)) < 0)
perr_exit("bind error");
return result;
}
int Connect(int fd, const struct sockaddr *addr, socklen_t len)
{
int result;
if((result = connect(fd, addr, len)) < 0)
perr_exit("connect error");
return result;
}
int Listen(int fd, int backlog)
{
int result;
if((result = listen(fd, backlog)) < 0)
perr_exit("listen error");
return result;
}
/*
size_t nbytes 读的字节数(范围);
ssize_t n 实际读到的字节数;
*/
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if((n = read(fd, ptr, nbytes)) == -1)
{
if(errno == EINTR)
goto again;
else
perr_exit("read error");
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if((n = write(fd, ptr, nbytes)) == -1)
{
if(errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
//参数3:应该读取的字节数
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft; //unsigned int 剩余未读字节数
ssize_t nread; //(signed) int 读一次实际读到的字节数
char *ptr;
ptr = vptr;
nleft = n;
while(nleft > 0)
{
if((nread = read(fd, ptr, nleft)) < 0)
{
if(errno == EINTR)
nread = 0;
else
return -1;
}
else if(nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while(nleft > 0)
{
if((nwritten = write(fd, ptr, nleft)) <= 0)
{
if(nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if(read_cnt <= 0)
{
again:
if((read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0)
{
if(errno = EINTR)
goto again;
return -1;
}else if(read_cnt == 0);
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
/*
readline --- fgets
传出参数vptr
*/
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for(n = 1; n < maxlen; n ++)
{
if((rc = my_read(fd, &c)) == 1)
{
*ptr++ = c;
if(c == '\n')
break;
}else if(rc == 0)
{
*ptr = 0;
return n - 1;
}else
return -1;
}
*ptr = 0;
return n;
}
int Fork()
{
int pid;
if((pid = fork()) < 0)
perr_exit("fork error");
return pid;
}
wrap.h:
#ifndef __WRAP_H_
#define __WRAP_H_
void perr_exit(const char *s);
int Socket(int domain, int type, int protocol);
int Accept(int fd, struct sockaddr *addr, socklen_t *len);
int Bind(int fd, const struct sockaddr *addr, socklen_t len);
int Connect(int fd, const struct sockaddr *addr, socklen_t len);
int Listen(int fd, int backlog);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
static ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *ptr, size_t maxlen);
int Fork();
#endif
server.c:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <ctype.h>
#include <strings.h>
#include "wrap.h"//包含自定义的函数
#define SERV_IP INADDR_ANY
#define SERV_PORT 8888
int main(void)
{
int serv_fd, clie_fd;
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
char buf[BUFSIZ], clie_ip_buf[BUFSIZ];
int n, i;
serv_fd = Socket(AF_INET, SOCK_STREAM, 0);//用自定义的Socket()
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERV_PORT);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
Bind(serv_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(serv_fd, 128);
clie_addr_len = sizeof(clie_addr);
clie_fd = Accept(serv_fd, (struct sockaddr *)&clie_addr, &clie_addr_len);//用自定义的Accept()
printf("client IP: %s, client port: %d\n", inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_ip_buf, sizeof(clie_ip_buf)), ntohs(clie_addr.sin_port));
while(1){
n = Read(clie_fd, buf, sizeof(buf));
for(i = 0; i < n; i++)
buf[i] = toupper(buf[i]);
Write(clie_fd, buf, n);
}
Close(serv_fd);
Close(clie_fd);
return 0;
}
client.c同样换用自定义的函数;
联合编译并执行:
为什么不直接:
因为联合编译时,如果修改其中一个.c文件,另一个未修改的不需要gcc -c,分开编译可以只编译修改的,在工程特别大的时候可以节省时间;
- 网络编程涉及的errno和错误处理
accept():
EINTER(Error INTERrupted):这个系统调用(system call)让一个被捕捉(caught)的信号(signal)打断了(interrupted);
ECONNABORTED(Error CONNection ABORTED):连接(connection)异常断开了(aborted);
以上两个ERROR都是在程序阻塞的时候被打断了,处理方式是要么重启即goto again,要么退出;connect()函数不会遇到这种ERROR,因为connect()是客户端主动发起的,不阻塞;
read():
返回值:
1、> 0:可能==sizeof(buf)也可能<sizeof(buf);
2、==0:数据读完,读到文件/管道/Socket的末尾,对面Socket写端关闭;
3、== -1:异常
1)errno == EINTR:被信号中断,此时重启或退出;
2)errno == EAGAIN或EWOULDBLOCK:非阻塞方式读,并且没有数据;
3)其他:出现错误,perr_exit();
- read()、write()函数的参数size_t、返回值ssize_t
size_t是自定义的描述size的无符号整型,应该读/写的范围;ssize_t是有符号整型,实际读/写的范围;
- 自己封装的Readn()函数什么时候用
从Socket里读数据,Socket的数据是从网络中接收到的,而以太网帧最大1500字节,如果我的需求是读4096字节,那么我用read()函数最多读1500字节就返回了,而我们要求读4096字节然后返回,因此自己封装一个Readn(),即通过重复调用read()实现读n个字节;
- 三次握手和四次握手
1、网络层是不稳定的,容易受硬件影响(现在随着技术进步,影响已经很小了),比如有路由器宕机就会影响网络层质量,也就是说网络层有丢包的风险;
2、由于网络层的不稳定性,传输层也分两类:
1)完全不弥补:顺应网络层的特点,以UDP为代表,面向无连接的报文传输,电报发报文的特点就是接收无应答,丢包不重传;
2)完全弥补:弥补网络层的缺陷,以TCP为代表,面向连接的数据包传输, 接收有应答,丢包会重传;
3、TCP既然是面向连接的,就要先建立连接再发数据,如果由于网络层的不稳定性导致连接断开,就得重新建立连接,发送完数据还要关闭连接;
4、上述建立连接的过程就是三次握手:
1)Client----SYN 1000(0)---->Server
客户端向服务端发起请求:包号是1000,字节数是0(即携带数据的长度是0字节);
2)Client<----SYN 8000(0)ACK 1001----Server
服务端响应客户端并向客户端发起请求:响应:确认收到1001之前的,下次发1001;请求:包号是8000,字节数是0;
3)Client----ACK 8001---->Server
客户端响应服务器:8001之前的包收到,从8001开始发吧;
5、三次握手完成,接下来就是发数据(不一定非得一来一回):
1)Client----1001(20)ACK 8001---->Server
客户端确响应务端并向服务端发数据:响应:确认收到8001之前的,下次发8001;发数据:包号是1001,长度是20(即1001~1020);
2)Client<----8001(40)ACK 1021----Server
服务端响应客户端并向客户端发数据:响应:确认收到1021之前的,下次发1021;发数据:包号是8001,长度是40(即8001~8040);
3)Client----ACK 8041----Server
客户端响应服务端:确认收到8041之前的,下次发8041;
6、发完之后关闭连接的过程就是四次握手:
为什么有4次:因为允许半关闭状态;什么是半关闭状态:一端关闭另一端不关闭,客户端请求关闭并收到服务端的确认后,客户端能接收并确认服务端的数据,但不再给服务端发数据;
1)Client----FIN 1021(0)ACK 8041---->Server
客户端请求关闭:包号是1021,字节数是0;并且确认8041;
2)Client<----ACK 1022----Server
服务端确认收到1021;此时客户端关闭,不再发数据,但能接收数据并发确认包;
3)Client<----FIN 8041(0)ACK 1022----Server
服务端请求关闭,包号是8041,字节数是0;并且确认1022;
4)Client----ACK 8042---->Server
客户端确认收到8042;此时服务端关闭,不再发数据;
注意从三次握手、发送数据到四次握手,请求序列号和确认序列号都是连贯的;
- 多进程Server
#include <stdio.h>
#include <stdlib.h> //exit()
#include <unistd.h> //close()
#include <arpa/inet.h> //网络编程有关函数
#include <strings.h> //bzero()
#include <ctype.h> //toupper()
#include <sys/wait.h> //signol()、WNOHANG
#include <signal.h> //SIGCHLD
#include "wrap.h" //自定义函数们
#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888
//用来处僵尸进程的
void wait_child(int signo)
{
while(waitpid(0, NULL, WNOHANG) > 0)
return;
}
int main(void)
{
int lfd, cfd;
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
pid_t pid;
char buf[BUFSIZ], clie_ip[BUFSIZ];
int n, i;
lfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);
serv_addr.sin_port = htons(SERV_PORT);
Bind(lfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(lfd, 128);
//无限循环,每连接一个客户端,就创建一个cfd与之连接,并创建一个子进程,产生分支:
//父进程的cfd没用了,close掉继续等待下一个;
//子进程的lfd没用了,退出循环去和客户端通信;
while (1)
{
clie_addr_len = sizeof(clie_addr);
cfd = Accept(lfd, (struct sockaddr *)&clie_addr, &clie_addr_len);
printf("%s:%d\n", inet_ntop(AF_INET, &clie_addr.sin_addr, clie_ip, sizeof(clie_ip)), ntohs(clie_addr.sin_port));
pid = Fork();
if(pid == 0)
{
close(lfd);
break;
}
else
{
close(cfd);
//用来处理僵尸进程
//僵尸进程是子进程死了没有被父进程回收,白白占据了资源
signal(SIGCHLD, wait_child);
}
}
if(pid == 0)
{
while(1)
{
n = Read(cfd, buf, sizeof(buf));
if(n == 0)//client closed
{
close(cfd);
return 0;
}
else
{
for(i = 0; i < n; i ++)
buf[i] = toupper(buf[i]);
Write(cfd, buf, n);
}
}
}
return 0;
}
不用signal()的话会有僵尸进程(STAT为Z+的进程):
- 多线程Server
#include <stdio.h>
#include <unistd.h> //STDOUT_FILENO
#include <string.h>
#include <arpa/inet.h> //网络编程有关函数
#include <ctype.h> //toupper()
#include <pthread.h>
#include "wrap.h" //自定义函数们
#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888
#define MAXLINE 8192
//自定义结构体,将socket的addr和fd合并
struct sock_info
{
struct sockaddr_in addr;
int fd;
};
void *process(void *arg)
{
int n, i;
struct sock_info *ts = (struct sock_info *)arg;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
while(1)
{
n = Read(ts->fd, buf, MAXLINE);
if(n == 0)
{
printf("client is closed...\n");
break;
}
printf("%s:%d\n", inet_ntop(AF_INET, &ts->addr.sin_addr, str, sizeof(str)), ntohs(ts->addr.sin_port));
Write(STDOUT_FILENO, buf, n);
for(i = 0; i < n; i ++)
buf[i] = toupper(buf[i]);
Write(ts->fd, buf, n);
}
close(ts->fd);
return (void *)0;
}
int main(void)
{
int listen_fd, connect_fd;
struct sockaddr_in serv_addr, clie_addr;
socklen_t clie_addr_len;
struct sock_info ts[256];
pthread_t tid;
int i = 0;
listen_fd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
inet_pton(AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr);
serv_addr.sin_port = htons(SERV_PORT);
Bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
Listen(listen_fd, 128);
printf("Accepting...\n");
//无限循环,主线程负责监听,收到连接后创建子线程与之通信
while (1)
{
clie_addr_len = sizeof(clie_addr);
connect_fd = Accept(listen_fd, (struct sockaddr *)&clie_addr, &clie_addr_len);
ts[i].addr = clie_addr;
ts[i].fd = connect_fd;
//创建子线程
pthread_create(&tid, NULL, process, (void *)&ts[i]);
//子线程设置为分离状态,防止僵尸线程
pthread_detach(tid);
i ++;
}
return 0;
}
引用pthread.h时不能直接链接:
因为pthread不是Linux默认的库,加个参数就可以了:
- TO BE CONTINUED. . .